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
  2. 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
  3. 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
  4. 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
  5. 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
  6. YOU MAY ALREADY KNOW RAILS / RUBY OBJECTS • Model

    • View • Controller • Mailer • Mailbox • Job • Validator • Channel • Storage • Uploader • Policy • Decorator • Input • …
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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)
  32. 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
  33. 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
  34. 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
  35. IDEAS ARE TEAM WORKS ACKNOWLEDGEMENTS • 五樓樓專業 5FPro 陳冠宏 Marzs

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