Let's Talk About DCI

Hi, I'm Jared

  • Full stack developer @ Bloc
  • GitHub: raderj89
  • Twitter: @JaredRader

This talk's inspiration

What is DCI?

separates the domain model (Data) from use cases (Context) and Roles that objects play (Interaction)

- Wikipedia

A 'hello world' example

  • Fat Models, Skinny Controllers

  • Rails Concerns

  • DCI

The Fat Models, Skinny Controllers Approach


# app/controllers/transfers_controller.rb
class TransfersController < ApplicationController

  def transfer
    @source_account = Account.find(params[:source_id])
    @destination_account = Account.find(params[:destination_id])
    amount = params[:transfer_amount]

    if @source_account.transfer_to(@destination_account, amount)
      flash[:success] = 'Transfer completed successfully!'
      redirect_to @source_account
    else
      flash[:error] = 'Something went wrong'
      render :transfer
    end
  end
end
# app/models/account.rb
class Account < ActiveRecord::Base
# == Schema Information
#
# Table name: accounts
#  id       :integer  not null, primary key
#  balance  :integer  default(0)

  def transfer_to(destination, amount)
    self.decrement(amount)
    destination.increment(amount)
  end

  def increment(amount)
    self.balance += amount
    self.save
  end
  
  def decrement(amount)
    self.balance -= amount
    self.save
  end
end

Fat Models

Down the rabbit hole...

The Concerns Approach



# app/models/concerns/transferrable.rb
module Transferrable
  extend ActiveSupport::Concern

  def transfer_to(destination, amount)
    self.decrement(amount)
    destination.increment(amount)
  end
end


# app/models/account.rb
class Account < ActiveRecord::Base
# == Schema Information
#
# Table name: accounts
#  id       :integer  not null, primary key
#  balance  :integer  default(0)

  include Transferrable
  include Depositable
  include Notifiable

  def increment(amount)
    self.balance += amount
    self.save
  end
  
  def decrement(amount)
    self.balance -= amount
    self.save
  end
end
  • Models still just as big
  • Dependent on Rails
  • No context

The DCI Approach


# app/controllers/transfers_controller.rb
class TransfersController < ApplicationController

  def transfer
    @source_account = Account.find(params[:source_id])
    @destination_account = Account.find(params[:destination_id])
    amount = params[:transfer_amount]

    if TransferringMoney.new(@source_account, @destination_account).execute_transfer(amount)
      flash[:success] = 'Transfer completed successfully!'
      redirect_to @source_account
    else
      flash[:error] = 'Something went wrong'
      render :transfer
    end
  end
end


# app/models/account.rb
class Account < ActiveRecord::Base
# == Schema Information
#
# Table name: accounts
#  id       :integer  not null, primary key
#  balance  :integer  default(0)

  def increment(amount)
    self.balance += amount
    self.save
  end
  
  def decrement(amount)
    self.balance -= amount
    self.save
  end
end


# app/contexts/transferring_money.rb
class TransferringMoney
  attr_reader :source, :destination

  def initialize(source, destination)
    @source = source
    @destination = destination

    assign_transferrable(@source)
  end

  def execute_transfer(amount)
    source.transfer_to(destination, amount)
  end

  private

  def assign_transferrable(source)
    source.extend(Transferrable)
  end

  module Transferrable
    def transfer_to(destination, amount)
      self.decrement(amount)
      destination.increment(amount)
    end
  end

  private_constant :Transferrable
end

Context

Data

Interaction

Role

Benefits:

  • Source code accurately reflects runtime
  • Contextual code is easier to understand
  • Models are actually skinny
  • Discourages feature envy

Potential Issues

  • extend invalidates the method cache
  • objects cannot be de-extended
class TransferringMoney
  attr_reader :source, :destination

  def initialize(source, destination)
    @source = source
    @destination = destination

    assign_transferrable
  end

  def execute_transfer(amount)
    source.transfer_to(destination, amount)
  end

  private

  def assign_transferrable
    @source = Transferrable.new(@source)
  end

  class Transferrable < SimpleDelegator
    def transfer_to(destination, amount)
      self.decrement(amount)
      destination.increment(amount)
    end
  end

  private_constant :Transferrable
end

Wrappers

  • Keeps the method cache intact

Pros

Cons

  • self is no longer an Account, it is a Transferrable
  • this can lead to AssociationTypeMismatch errors in ActiveRecord

Unbound Methods

# app/models/account.rb
class Account < ActiveRecord::Base
# == Schema Information
#
# Table name: accounts
#  id       :integer  not null, primary key
#  balance  :integer  default(0)

  def increment(amount)
    self.balance += amount
    self.save
  end
  
  def decrement(amount)
    self.balance -= amount
    self.save
  end

  def add_role(mod)
    @role = mod
  end

  def roles
    @role
  end

  def method_missing(method_name, *args, &block)
    if role.instance_methods.include?(method_name)
      role.instance_method(method_name).bind(self).call(*args, &block)
    else
      super
    end
  end
end


# app/contexts/transferring_money.rb
class TransferringMoney
  attr_reader :source, :destination

  def initialize(source, destination)
    @source = source
    @destination = destination

    assign_transferrable
  end

  def execute_transfer(amount)
    source.transfer_to(destination, amount)
  end

  private

  def assign_transferrable
    @source.add_role(Transferrable)
  end

  module Transferrable
    def transfer_to(destination, amount)
      self.decrement(amount)
      destination.increment(amount)
    end
  end

  private_constant :Transferrable
end
  • perhaps the most complex implementation
  • Best of both worlds: maintain self reference and keeps method cache intact

Pros

Cons

Discover More

Jared

  • Full stack developer @ Bloc
  • GitHub: raderj89
  • Twitter: @JaredRader
Made with Slides.com