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
endmodel
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
endapi, 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
endwhat is an operation
operation
class Payment::Create < Trailblazer::Operation
def process(params)
...
end
endoperation
class Payment::Create < Trailblazer::Operation
include Model
model Payment, :create
def process(params)
...
end
endoperation
class Payment::Create < Trailblazer::Operation
include Model
model Payment, :create
def process(params)
validate(params[:payment]) do
contract.save
end
end
endoperation
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
endcontroller
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
endclass 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
endoperation
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
endand 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
endlogic
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
Trailblazer
By kircxo
Trailblazer
- 426