Your browser doesn’t support the features needed to display this presentation.
A simplified version of this presentation follows.
For the best experience, please use one of the following browsers:
MVC
Where does the Business Logic go?
Have you seen me?
class Dojo < ActiveRecord::Base
validates_presence_of :sensei
validates_presence_of :name
after_create :requisition_katanas
before_validation_on_create :recommend
def requisition_katanas
if Katana.in_warehouse.count > 10
Katana.limit(10).each { |k| k.update(dojo: self) }
else
DojoMailer.send_katanas_needed_email(self)
end
end
def recommend
if has_sensei?
Ninjas.without_dojo.recommend(self)
else
Sensei.without_dojo.recommend(self)
end
end
end
Business logic in Models?
- Mixed concerns
- Data validation
- Data persistence
- Business logic
- Order of operations is unclear
- Difficult to reason about
- Models tightly coupled
- No logging trail
Have you seen me?
class DojoController < ApplicationController
def create
params = params.require(:dojo).allow(:name, :sensei_id)
@dojo = Dojo.new(params)
if @dojo.save
if @dojo.has_sensei?
Ninjas.without_dojo.recommend(@dojo)
else
Sensei.without_dojo.recommend(@dojo)
end
if Katana.in_warehouse.count > 10
Katana.limit(10).each { |k| k.update(dojo: @dojo) }
else
DojoMailer.send_katanas_needed_email(@dojo)
end
else
flash.now[:error] = "Could not create dojo"
render :new
end
end
end
Business logic in Controllers?
- Mixed concerns (request handling and biz logic)
- Long synchronous request
- High probability of failure
- Data inconsitency when partial failure
- No logging trail
Where does the business logic go?
- Model - Nope
- View - Nope
- Controller - Nope
- Mediator?
A Mediator
class Mediators::Dojo::Creator < Base
attr_reader :name, :sensei_id
def initialize(name:, sensei_id:)
@name, @sensei_id = name, sensei_id
end
def call
Dojo.transaction do
create_dojo
recommend_dojo
requisition_katanas
end
return @dojo
end
def create_dojo
@dojo = Dojo.create(name: name, sensei: sensei)
end
def recommend_dojo
# ...
end
def requisition_katanas
# ...
end
end
Improved Controller
class DojoController < ApplicationController
def create
params = params.require(:dojo).allow(:name, :sensei_id)
@dojo = Mediators::Dojo::Creator.run(params)
rescue
flash.now[:error] = "Could not create dojo"
render :new
end
end
Improved Model
class Dojo < ActiveRecord::Base
validates_presence_of :sensei
validates_presence_of :name
end
MVCM?
- Model - Now concerned only with validation and persistence
- View - Still only concerned with rendering
- Controller - Now concerned only with handling and responding to requests
- Mediator - Handles business logic
What's wrong now?
- We're still missing solid logging
- We still have one long synchronous request
Simple logging
class Mediators::Dojo::Creator < Mediators::Base
# ...
def create_dojo
log(creating_dojo: true) do
@dojo = Dojo.create(name: name, sensei_id: sensei_id)
log(dojo_id: dojo.id)
end
end
# ...
private
def log(data, &blk)
Pliny.log({
dojo_creator: true,
name: name,
sensei_id: sensei_id
}.merge(data), &blk)
end
end
Structured searchable logs:
2016-09-22T20:43:08.337698+00:00 54.226.135.103 local7.info app[web.1]: - d.123-450-33241 app=shuriken deployment=production name=foo sensei_id=123 creating_dojo at=start
2016-09-22T20:43:08.654278+00:00 54.226.135.103 local7.info app[web.1]: - d.123-450-33241 app=shuriken deployment=production name=foo sensei_id=123 dojo_id: 12345
2016-09-22T20:43:08.897738+00:00 54.226.135.103 local7.info app[web.1]: - d.123-450-33241 app=shuriken deployment=production name=foo sensei_id=123 creating_dojo at=finish
Do this in every mediator!
Introduce a Sidekiq Job
class Jobs::Dojo::Prepare
include Sidekiq::Worker
attr_reader :dojo_id
def perform(dojo_id)
@dojo_id = dojo_id
recommend_dojo
requisition_katanas
end
private
def dojo
@dojo ||= Dojo.find(dojo_id)
end
def recommend_dojo
# ...
end
def requisition_katanas
# ...
end
end
What's wrong with that?
- Job has mixed concerns
- Deserializing arguments into models
- Business logic
- There is no logging for asynchornous work
Improved Job
class Jobs::Dojo::Prepare
include Sidekiq::Worker
attr_reader :dojo_id
def perform(dojo_id)
@dojo_id = dojo_id
prepare_dojo
end
private
def dojo
@dojo ||= Dojo.find(dojo_id)
end
def prepare_dojo
Mediators::Dojo::Preparer.run(dojo) if dojo
end
end
MVCMJ?
- Model - Data validation and persistence
- View - Rendering
- Controller - Handling and responding to requests
- Mediator - Business logic
- Jobs - Deserializing queued work
Pros
- Separation of concerns
- Decoupled models
- Requests are served faster
- Extensible
- Ops friendly
Cons
- Additional files
- Additional lines of code