Created
May 16, 2012 15:18
-
-
Save alanq/2711153 to your computer and use it in GitHub Desktop.
DCI in ruby without injection - MoneyTransfer example
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
----------------------------------------- | |
Boilerplate code (in rails_app_root/lib) | |
----------------------------------------- | |
module ContextAccessor | |
def context | |
Thread.current[:context] | |
end | |
end | |
module Context | |
include ContextAccessor | |
attr_reader :role_player # allows a role to find its player | |
# Context setter is defined here so it's not exposed to roles (via ContextAccessor) | |
def context=(ctx) | |
Thread.current[:context] = ctx | |
end | |
# sets the current global context for access by roles in the interaction | |
def execute_in_context | |
old_context = self.context | |
self.context = self | |
return_object = yield | |
self.context = old_context | |
return_object | |
end | |
end # module Context | |
# A role contains only class methods and cannot be instantiated. | |
# Although role methods are implemented as public class methods, they only have | |
# access to their associated object while the role's context is the current context. | |
class Role | |
def initialize | |
raise "A Role should not be instantiated" | |
end | |
class << self | |
protected | |
include ContextAccessor | |
# retrieve role object from its (active) context's hash instance variable | |
def player | |
context.role_player[self] | |
end | |
# allow player object instance methods to be called on the role's self | |
def method_missing(method, *args, &block) | |
super unless context && context.is_a?(my_context_class) | |
if player.respond_to?(method) | |
player.send(method, *args, &block) | |
else # Neither a role method nor a valid player instance method | |
super | |
end | |
end | |
def my_context_class # a role is defined inside its context class | |
self.to_s.chomp(role_name).constantize | |
end | |
def role_name | |
self.to_s.split("::").last | |
end | |
end # Role class methods | |
end | |
----------------------------------------- | |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
Contexts (in rails_app_root/app/contexts) | |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
----------------------------------------- | |
class Account | |
include Context | |
def initialize(ledgers = []) | |
@role_player = {} | |
@role_player[Ledgers] = Array(ledgers) # association of the object ledgers (an array) with the role Ledgers | |
end | |
def balance() | |
execute_in_context do | |
Ledgers.balance | |
end | |
end | |
def increase_balance(amount) | |
execute_in_context do | |
Ledgers.add_entry 'depositing', amount | |
end | |
end | |
def decrease_balance(amount) | |
execute_in_context do | |
Ledgers.add_entry 'withdrawing', -1 * amount | |
end | |
end | |
# A role can use self or player to reference the obj associated with it | |
class Ledgers < Role | |
class << self | |
def add_entry(msg, amount) | |
player << LedgerEntry.new(:message => msg, :amount => amount) | |
end | |
def balance | |
player.collect(&:amount).sum | |
end | |
end # Role class methods | |
end # Role | |
end # Context | |
class MoneyTransfer | |
include Context | |
def initialize(source, destination, amount) | |
@role_player = {} | |
@role_player[Source] = source | |
@role_player[Destination] = destination | |
@role_player[Amount] = amount | |
end | |
def trans | |
execute_in_context do | |
Source.transfer @role_player[Amount] # so player not role will go to subcontext | |
end | |
end | |
class Source < Role | |
class << self | |
def transfer(amount) | |
log = Logger.new(STDOUT) | |
log.info "Source balance is #{Source.balance}" | |
log.info "Destination balance is #{Destination.balance}" | |
Destination.deposit amount | |
Source.withdraw amount | |
log.info "Source balance is now #{Source.balance}" | |
log.info "Destination balance is now #{Destination.balance}" | |
end | |
def withdraw(amount) | |
Source.decrease_balance amount | |
end | |
end | |
end | |
class Destination < Role | |
class << self | |
def deposit(amount) | |
Destination.increase_balance amount | |
end | |
end | |
end | |
class Amount < Role | |
end | |
end | |
------------------------------------------ | |
Domain Model (app/models/ledger_entry.rb) | |
DB table has amount:decimal & message:string | |
------------------------------------------ | |
class LedgerEntry < ActiveRecord::Base | |
attr_accessible :amount, :message | |
end | |
----------------------------- | |
Program (run in ruby console) | |
----------------------------- | |
l1 = LedgerEntry.new(:message=>'lodge',:amount=> 500) | |
l2 = LedgerEntry.new(:message=>'lodge',:amount=> 420) | |
source = Account.new([l1,l2]) | |
destination = Account.new() | |
context = MoneyTransfer.new(source, destination, 700) | |
context.trans |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yay Ruby!!