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.

61b55110e7f363bff43bcab8789930fb?s=128

tsechingho

July 27, 2019
Tweet

Transcript

  1. SUIT UP FOR 
 FRONTEND AND BACKEND 
 DEVELOPMENT 2019

    RUBY CONF TW 何澤清 TSE-CHING HO
  2. WHY SEPARATES FRONT / BACKEND DEVELOPMENT

  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
  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
  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
  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
  7. WHAT HELPS BACKEND DEVELOPMENT

  8. APPLICATION OBJECTS

  9. YOU MAY ALREADY KNOW RAILS / RUBY OBJECTS • Model

    • View • Controller • Mailer • Mailbox • Job • Validator • Channel • Storage • Uploader • Policy • Decorator • Input • …
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  24. FLOW CONTROL

  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
  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
  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
  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
  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
  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
  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
  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
  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
  34. HOW TO COOPERATE WITH FRONTEND DEVELOPMENT

  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
  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)
  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
  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
  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
  40. IDEAS ARE TEAM WORKS ACKNOWLEDGEMENTS • 五樓樓專業 5FPro 陳冠宏 Marzs

    • 五倍紅寶⽯石 5xRuby 蒼時弦也 Elct9620 • ⿈黃碼科技 Goldenio 王悉剛 SeanWang • ⿈黃碼科技 Goldenio 何澤俊 hotsechun