Created
October 26, 2011 06:45
-
-
Save st33n/1315636 to your computer and use it in GitHub Desktop.
DCI example in Ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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}" |
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.
Thanks for the reply. To be honest, it's probably just a matter of style and what works best with your environment / language / problem.
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
Hmmm... is there a particular reason for storing the "amount" within the context?
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.
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.