Slide 1

Slide 1 text

SUIT UP FOR 
 FRONTEND AND BACKEND 
 DEVELOPMENT 2019 RUBY CONF TW 何澤清 TSE-CHING HO

Slide 2

Slide 2 text

WHY SEPARATES FRONT / BACKEND DEVELOPMENT

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

WHAT HELPS BACKEND DEVELOPMENT

Slide 8

Slide 8 text

APPLICATION OBJECTS

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

FLOW CONTROL

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

HOW TO COOPERATE WITH FRONTEND DEVELOPMENT

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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)

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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