an approach to scale monolithic codebases

who am i

  • started rails with 2.2.2
  • worked on between 10-15 rails codebases
  • some very well architected
  • some not so
 

why

 
  • the amount of code grows and new organization patterns are needed
  • code duplication: api, web, ajax
  • testing overhead
 

what

 
  • more object oriented
  • new programmed patterns
  • help with orchestration
 

why of trailblazer

 
  • rails is for getting a project/prototype up quickly
  • rails is for getting programmers familiar quickly
  • rails eases programmers into mvc
  • trailblazer is to handle complexity past rails
  • these are from experience
  • patterns, can go both directions
  • definitely not only possibilities
 

what do you do when you begin to go past rails' patterns?

commands
pundit
decorators/presenter
DelegateClass
command operation
pundit policy
decorators/presenter cells
DelegateClass operation
 

what we'll focus on

 
  • creating more reusable contexts of interaction
  • passing simple objects between layers
  • use polymorphism over conditionals
  • reduce test setup
 

our sample domain

 

medical

 

payments

 

payment

 
  • you just pay right?
  • gets paid by client
  • unless insurance, then part gets paid by insurance
  • different parts get paid dependent on insurance
  • notifications need to be sent to insurance

discretion of records

  • what can doctor see of medical record
  • lab
  • billing agent
  • receptionist
 

controllers

 

our default orchestrator

 

example

class PaymentController < ApplicationController
  def show
    @payment = Payment.build(params[:payment])
  end

  def create
    @payment = Payment.create!(params.require(:payment).permit(:amount, :patient_id, :visit_id)

    CreditCardProcessor.new(payment: @payment).run! if @payment.patient_portion
  end
end
 

model

class Payment < ActiveRecord::Base

  belongs_to :patient
  belongs_to :visit

  validates :amount, numericality: { less_than: 5_000 }

  before_create :handle_insurance, if: ->(payment) { payment.patient.insurance? }

  def patient_portion?; ...; end

  def insurance_portion?; ...; end

  private

  def handle_insurance
    ins_pay = InsuranceBill.create!(patient: patient, visit: visit) if insurance_portion?
    InsuranceNotification.create!(payment: ins_pay) if insurance_portion?
  end
end

api, ajax, import

 

controller

class PaymentController < ApplicationController
  def show
    @payment = Payment.build
  end

  def create
    @payment = Payment.build(params[:payment])

    CreditCardProcessor.new(payment: @payment).run! if @payment.patient_portion

    patient = @payment.patient

    if patient.insurance?
      ins_pay = InsuranceBill.create!(patient: patient, visit: visit) if @payment.insurance_portion
      InsuranceNotification.create!(payment: ins_pay) if @payment.insurance_portion
    end
  end
end

what is an operation

 

operation

class Payment::Create < Trailblazer::Operation
  def process(params)
    ...
  end
end
 

operation

class Payment::Create < Trailblazer::Operation
  include Model
  model Payment, :create

  def process(params)
    ...
  end
end
 

operation

class Payment::Create < Trailblazer::Operation
  include Model
  model Payment, :create

  def process(params)
    validate(params[:payment]) do
      contract.save
    end
  end
end
 

operation

class Payment::Create < Trailblazer::Operation
  include Model
  model Payment, :create

  contract do
    property :patient_id, validates: { presence: true }
    property :visit_id, validates: { presence: true }

    property :amount, validates: { numericality: { less_than: 5_000 } }
  end

  def process(params)
    validate(params[:payment]) do
      contract.save
    end
  end
end
 

operation

class Payment::Create < Trailblazer::Operation
  include Model
  model Payment, :create

  contract do
    property :patient_id, validates: { presence: true }
    property :visit_id, validates: { presence: true }

    property :amount, validates: { numericality: { less_than: 5_000 } }
  end

  def process(params)
    validate(params[:payment]) do
      contract.save
      CreditCardProcessor.new(payment: model).run! if model.patient_portion
    end
  end
end
 

controller

class PaymentController < ApplicationController
  def show
    @payment = Payment.build
  end

  def create
    @payment = Payment.build(params[:payment])

    patient = @payment.patient

    CreditCardProcessor.new(payment: @payment).run! if @payment.patient_portion

    if patient.insurance?
      ins_pay = InsuranceBill.create!(patient: patient, visit: visit) if @payment.insurance_portion
      InsuranceNotification.create!(payment: ins_pay) if @payment.insurance_portion
    end
  end
end
 

operation

class Payment::Create < Trailblazer::Operation
  include Model
  model Payment, :create

  contract do
    property :patient_id, validates: { presence: true }
    property :visit_id, validates: { presence: true }

    property :amount, validates: { numericality: { less_than: 5_000 } }
  end

  def process(params)
    validate(params[:payment]) do
      contract.save
      CreditCardProcessor.new(payment: @payment).run! if @payment.patient_portion

      if model.patient.insurance?
        ins_pay = InsuranceBill.create!(patient: patient, visit: visit) if model.insurance_portion
        InsuranceNotification.create!(payment: ins_pay) if model.insurance_portion
      end

    end
  end
end
class Payment::Create < Trailblazer::Operation
  include Model
  model Payment, :create

  contract do
    property :patient_id, validates: { presence: true }
    property :visit_id, validates: { presence: true }

    property :amount, validates: { numericality: { less_than: 5_000 } }
  end

  builds -> (params) do
    patient = Patient.find(params[:payment][:patient_id])
    return Insured if patient.insurance?
    return self
  end

  def process(params)
    validate(params[:payment]) do
      contract.save
      CreditCardProcessor.new(payment: @payment).run! if @payment.patient_portion
    end
  end

  class Insured < self
    def process(params)
      validate(params[:payment]) do
        super(params)

        ins_pay = InsuranceBill.create!(patient: patient, visit: visit) if model.insurance_portion
        InsuranceNotification.create!(payment: ins_pay) if model.insurance_portion
      end
    end
  end
end

operation

new controller

class PaymentController < ApplicationController
  def show
    form Payment::Create
  end

  def update
    run Payment::Create do
      redirect_to home_path
    end

    render :show
  end
end
 

new model

class Payment < ActiveRecord::Base

  belongs_to :patient

  def patient_portion?; ...; end

  def insurance_portion?; ...; end

end

so? you just moved code

  • run Payment::Create
  • form Payment::Create
  • Payment::Create.run(patient: {amount: 1_000}})
  • no more build(:payment)...
  • PaymentController.new.update == FAIL

main concepts

  • use simple data structures to go between layers
  • use polymorphism over conditionals
  • made our models skinnier
  • didn't resort to concerns

cells

let's make the view layer object oriented

current problems

  • helpers are global
  • controller IVARs allow spooky action at distance
  • magic context created from controller
  • attempt to use presenters to create smaller contexts

cells are like a presenter with a template

what does one look like?

class Payment
  class Cell < Cell::Concept

    include TimeAgoHelper

    property :amount
    property :created_at

    def show
      render
    end

    private

    def user
      options[:user]
    end

    def receipt_image
      img_tag model.images.resize(:small), alt: "Payment Receipt"
    end

  end
end

and the view

<div>
  <div><%= receipt_image %></div>
  <span><%= number_to_currency amount %></span>
  <% if user.admin? %><%= link_to Edit, edit_path %><% end %>
  <span>Created: <%= time_ago(created_at) %></span>
</div>
  
<%= concept("payment/cell, payment, user: acting_user) %>

composition

class Patient
  class Cell < Cell::Concept

    def show
      latest_payment = concept("payment/cell", patient.payments.last)

      '<span class="special">' + render + '</span>' + latest_payment
    end

  end
end

logic

class Payment
  class Cell < Cell::Concept

    def show
      render
    end

    private

    def late?
      created_at < 1.month.ago
    end

    def background_color
      late? ? "red" : "green"
    end

  end
end
<div class="background-color: <%= background_color %>">
  <%= number_to_currency amount %>
</div>

main concepts

  • context is model, not controller action
  • less global state
  • composing contexts

contexts

  • moving context of an interaction out of controller action
  • moving context of a view out of controller action
  • basically decoupling contexts from having to take place in the structure of the  framework

benefits of contexts

  • are easy to subclass (not actions/ui elements)
  • can be subclassed
  • can be tested independent of framework overhead
  • better isolation and separation of concerns
  • new reusable code not stuck in models or controller concerns

do i need to convert

whole project?

should we merge trailblazer and rails?

do we need another

dependency just for this

command operation
pundit policy
decorators/presenter cells
DelegateClass operation

how to start

  • you can start in as small of a place as you can think
  • this can be added in any increments you want
  • only use where you need/feel pain

Where now?

  • http://trailblazer.to
  • Trailblazer: A New Architecture for Rails

want help?

@kircxo

or

kircxo@gmail.com

 

source: https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

Made with Slides.com