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:

The Mediator Pattern


Memphis Ruby

9/22/2016

@joshwlewis - @heroku


joshwlewis.com/slides/mediator-pattern

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?

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?

Where does the business logic go?

The Mediator Pattern

With the mediator pattern, communication between objects is encapsulated with a mediator object. Objects no longer communicate directly with each other, but instead communicate through the mediator. This reduces the dependencies between communicating objects, thereby lowering the coupling.

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?

What's wrong now?

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

Improved 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
    create_dojo
    prepare_dojo
    return @dojo
  end

  def create_dojo
    # ...
  end

  def prepare_dojo
    Jobs::Dojo::Prepare.perform_async(dojo_id)
  end

  # ...
end

What's wrong with that?

A New Mediator

class Mediators::Dojo::Preparer < Mediators::Base
  attr_reader :dojo
  def initialize(dojo)
    @dojo = dojo
  end

  def call
    log(prepare_dojo: true) do
      recommend_dojo
      requisition_katanas
    end
  end

  def recommend_dojo
    # ...
  end

  def requisition_katanas
    # ...
  end

  # ...
end

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?

Pros

Cons

Additional Reading