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
Let's Talk About DCI
By Jared Rader
Let's Talk About DCI
An intro to and discussion of DCI
- 1,822