Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Suit up for frontend and backend development

Suit up for frontend and backend development

This talk will tell how to use gems and self crafted modules to assemble a rails application suitable for frontend/backend separated development. It also can apply to traditional rails application. This talk focus on backend development and 9 kinds of application objects will be discussed.

tsechingho

July 27, 2019
Tweet

More Decks by tsechingho

Other Decks in Programming

Transcript

  1. SUIT UP FOR 

    FRONTEND AND BACKEND 

    DEVELOPMENT
    2019 RUBY CONF TW
    何澤清
    TSE-CHING HO

    View full-size slide

  2. WHY SEPARATES
    FRONT / BACKEND
    DEVELOPMENT

    View full-size slide

  3. SINGLE PAGE APPLICATION IS POPULAR
    END USER LOVES INTERACTIVE WEB UI
    • Team work is important
    • It takes time to do things well
    • Complex UX interactions require tons of javascripts codes
    • Stylesheets (CSS) become a kind of arts
    • Data flow becomes key point to render UI efficiently
    • Features / requirements come quick and urgent
    • Strategy to implement codes as earlier as possible

    View full-size slide

  4. THESE ARE NOT ENOUGH
    FRONTEND DEVELOPMENT IN RAILS
    • Have UI wireframe to communicate
    • Fake data is enough to start UI development without database design
    • Use webpacker to build javascript environment
    • Use axios / rxjs as ajax library to chain promises in orders
    • Use Rails as backend API server
    • API controllers based on ActionController::API
    • JSON rendered by JBuilder
    • Follow JSON API specifications

    View full-size slide

  5. SIMPLE, CLEAN & UNIT TEST IS EASY
    EVERY DEVELOPER LIKES COMPONENTS
    • React.js - Redux
    • Actions
    • Components
    • Containers
    • Reducers
    • Styles
    • Locales
    • Vue.js - vue file
    • Template
    • Scripts
    • Styles
    • AMP
    • Custom DOM + JS Lib
    • Styles

    View full-size slide

  6. UI FEATURES CHANGES OFTEN
    BACKEND DEVELOPMENT BOMB
    • Database scheme is NOT match new features of UI
    • Query data issues (ex: joins, grouping, N+1)
    • Validations are a big issue for backend to fit UI
    • After callbacks are hard to adjust for ( new / old ) features or
    workflows
    • Update data in multiple models at one time
    • API endpoint of different versions may have similar business logic
    • Keep codes clean, separated and maintainable become harder

    View full-size slide

  7. WHAT HELPS
    BACKEND DEVELOPMENT

    View full-size slide

  8. APPLICATION
    OBJECTS

    View full-size slide

  9. YOU MAY ALREADY KNOW
    RAILS / RUBY OBJECTS
    • Model
    • View
    • Controller
    • Mailer
    • Mailbox
    • Job
    • Validator
    • Channel
    • Storage
    • Uploader
    • Policy
    • Decorator
    • Input
    • …

    View full-size slide

  10. NAMESPACE IS IMPORTANT
    APPLICATION LEVEL BASE OBJECTS
    • app/calculators/application_calculator.rb
    • app/contexts/application_context.rb
    • app/forms/application_form.rb
    • app/generators/application_generator.rb
    • app/operators/application_operator.rb
    • app/presenters/application_presenter.rb
    • app/services/application_service.rb
    • app/transformers/application_transformer.rb
    • app/values/application_value.rb
    All Based on Active::Model
    Based on gems if needed
    Write your won concerns or libraries
    Naming business logic well with namespace

    View full-size slide

  11. VALUE IN, VALUE OUT
    CALCULATOR
    • Values:

    Number, String, Hash, Array,
    Value Object
    • Count values:

    Amount, Quantity, Weight,
    Rate, Day, Rules
    • No Insert / Update / Delete
    • Repeatable & Consistent
    class ApplicationCalculator
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    def initialize(accessors = {})
    assign_attributes(accessors || {})
    end
    def perform
    # raise NotImplementedError
    end
    end

    View full-size slide

  12. TO BE OR NOT TO BE
    VALUE
    • Definition of values:

    Array, Hash, Object
    • Specific Data structure
    • Configurations / Preferences
    • Rules
    • Address / PhoneNumber
    • Behave by parameters
    • Could be Singleton
    • File source: CSV / YAML
    class ApplicationValue
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    def initialize(accessors = {})
    assign_attributes(accessors || {})
    end
    end

    View full-size slide

  13. FIND & QUERY
    CONTEXT
    • Find a record / value
    • Query collection by conditions
    • Relations / Scopes / Sort /
    Pagination
    • Arel is preferred
    • subquery is easier
    • where is more flexible
    • Be careful with N+1
    • File source: CSV / YAML
    class ApplicationContext
    class << self
    def attributes; end
    def permits; end
    end
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    def initialize(accessors = {})
    assign_attributes(accessors || {})
    end
    def perform
    # raise NotImplementedError
    end
    end

    View full-size slide

  14. CREATION IS NATURE
    GENERATOR
    • Data generation
    • CSV / Excel / PDF
    • Kinds of reports
    • Presenter is partner
    • Work with Job / Mailer
    • Factory
    class ApplicationGenerator
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    def initialize(accessors = {})
    assign_attributes(accessors || {})
    end
    def perform
    # raise NotImplementedError
    end
    end

    View full-size slide

  15. UI SAYS …
    PRESENTER
    • Rails view
    • Query: form / result
    • New / Edit: form
    • Show: sections
    • Data structure
    • Report: CSV / Excel
    • Chart
    • JSON for Outbound API
    • JBuilder / Serializer
    class ApplicationPresenter
    extend ActiveModel::Translation
    include ActiveModel::AttributeAssignment
    def initialize(accessors = {})
    assign_attributes(accessors || {})
    end
    end

    View full-size slide

  16. class ApplicationForm
    class << self
    def attributes; end # Array
    def permits; end # Array
    end
    extend ActiveModel::Callbacks
    include ActiveModel::Validations
    include ActiveModel::Validations::Callbacks
    define_model_callbacks :save
    def initialize(accessors = {})
    assign_attributes(accessors || {})
    end
    def save
    # raise NotImplementedError
    end
    end
    VALIDATION & PRESENTATION
    FORM
    • Validate
    • Attributes & Permits
    • Request params & 

    Strong parameter
    • Errors / Warnings / Notices 

    based on ActiveModel::Errors
    • Query / Create / Update
    • Build / Load attributes
    • Display
    • Builder = Presenter(s) + Form(s)
    • Plain / Nested form: fields_for

    View full-size slide

  17. FORM BUILDER = PRESENTER(S) + FORM(S)
    class SingleFormPresenter < ::ApplicationPresenter
    class << self
    def form_accessors(permits)
    accessors = permits.reject do |permit|
    %w[Symbol String].exclude? permit.class.name
    end
    attr_accessor(*accessors)
    end
    end
    include Rails.application.routes.url_helpers
    attr_reader :form
    delegate :errors, :valid?, to: :form
    def assign_form_attributes(form)
    @form = form
    self.class.form_accessors form.class.permits
    assign_attributes form.attributes.slice(*form.class.permits)
    end
    def persisted?
    false
    end
    end

    View full-size slide

  18. SAVING & NESTING
    FORM
    • Persist Data
    • Multiple models saving
    • Transaction: DB lock / uniqness
    validation / unique index
    • Condition & order of Save
    callback
    • Nested form
    • Complexity & Performance
    • Parameters / data mapping
    • Model finding condition of child
    form
    after_save :update_status
    def initialize(params, accessors = {})
    ......
    end
    def save
    return false if invalid?
    ActiveRecord::Base.transaction do
    run_callbacks :save { persist! }
    end
    errors.empty?
    end

    View full-size slide

  19. PATTERN - UPDATE FORM
    module Courses
    module Document
    class UpdateForm < BaseForm
    class << self
    ......
    end
    include ::HasPersistedRecordConcern
    has_persisted_record :document, class_name: 'Courses::Document'
    validates :document, presence: true
    after_save :update_status
    def initialize(params, accessors = {})
    attributes = filter_persisted_attributes_of_document_by(id: params[:document_id])
    super(attributes.merge(params))
    assign_attributes(accessors || {})
    end
    def save
    return false if invalid?
    ActiveRecord::Base.transaction do
    run_callbacks :save { persist! }
    end
    errors.empty?
    end
    end
    end
    end

    View full-size slide

  20. EXTEND ABILITIES OF FORMS
    module HasPersistedRecordConcern
    extend ActiveSupport::Concern
    class_methods do
    def has_persisted_record(name, class_name: nil, includes: nil)
    class_name ||= name.to_s.classify
    klass = class_name.constantize
    attr_accessor "#{name}_id"
    attr_writer name
    define_method name do
    if instance_variable_get(:"@#{name}").blank?
    id = instance_variable_get(:"@#{name}_id")
    object = klass.includes(includes).find_by id: id
    instance_variable_set(:"@#{name}", object) unless object.nil?
    end
    instance_variable_get(:"@#{name}")
    end
    define_method :"load_persisted_attributes_of_#{name}_by" do |id:|
    instance_variable_set(:"@#{name}_id", id)
    object = send(name)
    return {} if object.nil?
    attrs = object.attributes.symbolize_keys
    return attrs unless self.class.respond_to? :permits
    attrs.slice(*self.class.permits)
    end
    end
    end
    end

    View full-size slide

  21. MULTIPLE IN, ONE OUT
    TRANSFORMER
    • Request Parameters
    • Persisted attributes
    • Merge Data
    • Create / Update / Destroy
    • Sync Data
    • Multiple Inbound API data
    • Transfer Data (to Form)
    • Clean attributes
    • Valid format
    class ApplicationTransformer
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    include ActiveModel::Validations::Callbacks
    def initialize(params, accessors = {})
    @params = params
    assign_attributes(accessors || {})
    end
    def perform
    # raise NotImplementedError
    end
    end

    View full-size slide

  22. 3RD PARTY API LOVER
    SERVICE
    • API Communication
    • No Data Manipulation
    • Handle Request / Response
    • Errors of Data / Network
    • Work with Form / Transformer
    • Inquiry / Export
    • No Insert / Update / Delete
    • Import / Sync
    • Part of Operator
    class ApplicationService
    include ActiveModel::Validations
    include ActiveModel::Validations::Callbacks
    def run
    # raise NotImplementedError
    end
    end

    View full-size slide

  23. WORK FLOW MASTER
    OPERATOR
    • Business logics
    • Validation / Errors
    • Callback
    • Create / Update Action
    • Parameters: Transformer
    • Data: Context / Service
    • Form / Presenter
    • Operator Chain
    class ApplicationOperator
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    include ActiveModel::Validations::Callbacks
    def initialize(accessors = {})
    assign_attributes(accessors || {})
    end
    def perform
    # raise NotImplementedError
    end
    end

    View full-size slide

  24. FLOW CONTROL

    View full-size slide

  25. def show
    context =
    Courses::Document::QueryContext.new params, user: current_user
    context.perform
    @document_presenter =
    Courses::Document::SinglePresenter.new context.result
    end
    SHOW ACTION
    NO VIEW HELPERS

    View full-size slide

  26. SEARCH ACTION
    def search
    form =
    Courses::Document::QueryForm.new params, user: current_user
    form.save
    @form_presenter =
    Courses::Document::QueryFormPresenter.new form
    @result_presenter =
    Courses::Document::QueryResultPresenter.collect form.result
    end
    PAGINATION
    FORM BUILDER
    QUERY CONTEXT

    View full-size slide

  27. INDEX ACTION
    def index
    # search section
    form =
    Courses::Document::QueryForm.new params, user: current_user
    form.save
    @form_presenter =
    Courses::Document::QueryFormPresenter.new form
    @result_presenter =
    Courses::Document::QueryResultPresenter.collect form.result
    # other sections......
    end

    View full-size slide

  28. NEW ACTION
    def new
    operator =
    Courses::Document::CreateOperator.new params, current_user
    if operator.course.nil?
    redirect_to courses_path
    elsif operator.document&.persisted?
    redirect_to edit_course_document_path(
    operator.course,
    operator.document
    )
    else
    @form_presenter = operator.form_presenter
    end
    end
    NEW OPERATOR
    IF NEEDED

    View full-size slide

  29. CREATE ACTION
    def create
    operator =
    Courses::Document::CreateOperator.new params, current_user
    if operator.perform
    flash[:success] = I18n.t('.create.success')
    redirect_to course_document_path(
    operator.course,
    operator.document
    )
    else
    flash.now[:error] = operator.errors.full_messages.to_sentence
    @form_presenter = operator.form_presenter
    render :new
    end
    end

    View full-size slide

  30. EDIT ACTION
    def edit
    operator =
    Courses::Document::UpdateOperator.new params, current_user
    if operator.course.nil?
    redirect_to courses_path
    elsif operator.document.nil?
    redirect_to new_course_document_path(operator.course)
    else
    @form_presenter = operator.form_presenter
    end
    end
    EDIT OPERATOR
    IF NEEDED

    View full-size slide

  31. UPDATE ACTION
    def update
    operator =
    Courses::Document::UpdateOperator.new params, current_user
    if operator.perform
    flash[:success] = I18n.t('.update.success')
    redirect_to course_document_path(
    operator.course,
    operator.document
    )
    else
    flash.now[:error] = operator.errors.full_messages.to_sentence
    @form_presenter = operator.form_presenter
    render :edit
    end
    end

    View full-size slide

  32. DESTROY ACTION
    def destroy
    operator =
    Courses::Document::DestroyOperator.new params, current_user
    if operator.perform
    flash[:success] = I18n.t('.destroy.success')
    else
    flash[:error] = operator.errors.full_messages.to_sentence
    redirect_to course_documents_path(operator.course)
    end
    end
    VALIDATION

    View full-size slide

  33. ONE TASK, ONE OPERATOR
    OPERATOR IS EVERYWHERE
    • Controller actions
    • After callbacks of object
    • Create / Update other models
    • Rake
    def update_status
    operator =
    Course::Document::UpdateStatusOperator.new(document: document)
    operator.perform
    errors.merge! operator.errors
    end
    CALCULATORS

    View full-size slide

  34. HOW TO
    COOPERATE WITH
    FRONTEND DEVELOPMENT

    View full-size slide

  35. EACH OPERATION IS AN OBJECT
    BUILD FIRST, COMBINE LATER
    • Build business logic as objects first
    • Naming objects with good namespace (concept) ( usually 3~4 layers )
    • Make clear inputs and outputs of objects
    • Unit test objects as early as possible
    • Combine work flow by operators later
    • Use flow chart to make sure how things happen
    • Let objects perform by order in operator
    • Define features of JSON presenter and API operator with frontend

    View full-size slide

  36. IT’S EASY FOR EVERYONE
    DEFINE THE WORK FLOW
    • Payment::CreditCard::CreateOperator.new(order)
    • form = Payment::CreditCard::CreateForm.new(order)
    • service = ECPay::CreditCard::CreateService.new(form)
    • Form = Order::Payment::UpdateForm.new(service.attributes)
    • Invoice::Triplicate::CreateOperator.new(order)
    • form = Invoice::Triplicate::CreateForm.new(order)
    • service = ECPay::Invoice::CreateService.new(form)

    View full-size slide

  37. IN RUBY CONF TW
    I LEARNED YESTERDAY
    • Take your presenter as kind of data store in react.js
    • Unit test your presenter (store) is test part of views
    • ActionView::Component
    • Rethinking the View Layer with Components

    - Joel Hawksley from GitHub
    • Unit test your partial file of views

    View full-size slide

  38. GROUP OBJECTS
    ENGINES
    • Places
    • Rails.root/engines
    • Rails.root/vendor
    • Gemfile
    • Use path for engines in
    monolithic repository
    • Use gem for engines in
    standalone repository
    # Gemfile
    path 'engines' do
    gem 'invoice'
    end

    View full-size slide

  39. SOMETHING STILL NOT CHANGED
    BACKEND DEVELOPMENT CHANGES
    • Without knowing UI design
    • Can write business logic objects
    • Can write work flow operators
    • UI design is still important for database design
    • Query conditions should be reasonable in UI
    • Query efficiency are affected by UI
    • API efficiency and reliability is more important

    View full-size slide

  40. IDEAS ARE TEAM WORKS
    ACKNOWLEDGEMENTS
    • 五樓樓專業 5FPro 陳冠宏 Marzs
    • 五倍紅寶⽯石 5xRuby 蒼時弦也 Elct9620
    • ⿈黃碼科技 Goldenio 王悉剛 SeanWang
    • ⿈黃碼科技 Goldenio 何澤俊 hotsechun

    View full-size slide