Skip to content

Instantly share code, notes, and snippets.

@st33n
Created October 26, 2011 06:45
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save st33n/1315636 to your computer and use it in GitHub Desktop.
Save st33n/1315636 to your computer and use it in GitHub Desktop.
DCI example in Ruby
#!/usr/bin/env ruby
# Lean Architecture example in Ruby - with ContextAccessor
# This example keeps interaction state in a "current context", represented
# by a ContextAccessor module. This can be mixed in to any class that needs
# access to the current context. It is implemented as a thread-local variable.
module ContextAccessor
def context
Thread.current[:context]
end
def context=(ctx)
Thread.current[:context] = ctx
end
# Pass a block to this to have it executed with this context set
def in_context
old_context = self.context
self.context = self
yield
self.context = old_context
end
end
# Model class with no external dependenices. Includes a simple find method
# to create and store instances given an id - for illustration purposes only.
class Account
attr_reader :account_id, :balance
def initialize(account_id)
@account_id = account_id
@balance = 0
end
def withdraw(amount)
raise "Insufficient funds" if amount < 0
@balance -= amount
end
def deposit(amount)
@balance += amount
end
def update_log(msg, date, amount)
puts "Account: #{inspect}, #{msg}, #{date.to_s}, #{amount}"
end
def self.find(account_id)
@@store ||= Hash.new
return @@store[account_id] if @@store.has_key? account_id
if :savings == account_id
account = SavingsAccount.new(account_id)
account.deposit(100000)
elsif :checking == account_id
account = CheckingAccount.new(account_id)
else
account = Account.new(account_id)
end
@@store[account_id] = account
account
end
end
class Creditor
attr_accessor :amount_owed, :account
def self.find(name)
@@store ||= Hash.new
return @@store[name] if @@store.has_key? name
if :baker == name
creditor = Creditor.new
creditor.amount_owed = 50
creditor.account = Account.find(:baker_account)
elsif :butcher == name
creditor = Creditor.new
creditor.amount_owed = 90
creditor.account = Account.find(:butcher_account)
end
creditor
end
end
module MoneySource
include ContextAccessor
def transfer_out
raise "Insufficient funds" if balance < context.amount
withdraw context.amount
context.destination_account.deposit context.amount
update_log "Transfer Out", Time.now, context.amount
context.destination_account.update_log "Transfer In", Time.now, context.amount
end
def pay_bills
creditors = context.creditors.dup
creditors.each do |creditor|
TransferMoneyContext.execute(creditor.amount_owed, account_id, creditor.account.account_id)
end
end
end
# Implementation of Transfer Money use case
class TransferMoneyContext
attr_reader :source_account, :destination_account, :amount
include ContextAccessor
def self.execute(amt, source_account_id, destination_account_id)
TransferMoneyContext.new(amt, source_account_id, destination_account_id).execute
end
def initialize(amt, source_account_id, destination_account_id)
@source_account = Account.find(source_account_id)
@source_account.extend MoneySource
@destination_account = Account.find(destination_account_id)
@amount = amt
end
def execute
in_context do
source_account.transfer_out
end
end
end
class PayBillsContext
attr_reader :source_account, :creditors
include ContextAccessor
def self.execute(source_account_id, creditor_names)
PayBillsContext.new(source_account_id, creditor_names).execute
end
def initialize(source_account_id, creditor_names)
@source_account = Account.find(source_account_id)
@creditors = creditor_names.map do |name|
Creditor.find(name)
end
end
def execute
in_context do
source_account.pay_bills
end
end
end
class SavingsAccount < Account
end
class CheckingAccount < Account
end
TransferMoneyContext.execute(300, :savings, :checking)
TransferMoneyContext.execute(100, :checking, :savings)
puts "Savings: #{Account.find(:savings).balance}, Checking: #{Account.find(:checking).balance}"
# Now pay some bills
PayBillsContext.execute(:checking, [ :baker, :butcher])
puts "After paying bills, checking has: #{Account.find(:checking).balance}"
puts "Baker and butcher have #{Account.find(:baker_account).balance}, #{Account.find(:butcher_account).balance}"
@randito
Copy link

randito commented May 6, 2012

Hmmm... is there a particular reason for storing the "amount" within the context?

def transfer_out
    raise "Insufficient funds" if balance < context.amount  
    withdraw context.amount
    context.destination_account.deposit context.amount
    update_log "Transfer Out", Time.now, context.amount
    context.destination_account.update_log "Transfer In", Time.now, context.amount
  end

In my opinion, I would prefer the MoneySource#transfer_out method to take a parameter. It just seems more straightforward, and I don't see the value of hiding the parameters within the context.

def transfer_out(amount)

It seems that the code is more readable if the interactions (i.e. MoneySource) take a parameter as opposed to placing it within the context. I think the method is more explicit if you place the parameters for that method in the method signature. Someone glancing at this method has an immediate idea of what's going on.

@st33n
Copy link
Author

st33n commented May 7, 2012

The example follows Jim Coplien's C++ examples and are structured the same way. I don't have enough practical experience with DCI to say what makes more sense, but it's something you might explore on the mailing lists or with Jim directly.

@randito
Copy link

randito commented May 7, 2012

Thanks for the reply. To be honest, it's probably just a matter of style and what works best with your environment / language / problem.

@hakanderyal
Copy link

In the book Lean Architecture for Agile Software Development by Jim Coplien, he tells about 4 different ways to pass the information in the context to the relevant objects. This is the recommended method, using a global context object. In the end, it's a matter of style as you said.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment