Skip to content

Instantly share code, notes, and snippets.

@rinaldifonseca
Forked from alanq/mt.rb
Created June 7, 2012 20:17
Show Gist options
  • Save rinaldifonseca/2891308 to your computer and use it in GitHub Desktop.
Save rinaldifonseca/2891308 to your computer and use it in GitHub Desktop.
DCI in ruby without injection - MoneyTransfer example
# The code can be seen here: gist.github.com/2711153 (it does use a small bit
# from the Rails framework, just for the persisted LedgerEntry and to create
# attribute readers). Some notes about the code:
# - Each Role is a class, having only (public) class methods. To help
# avoid them being called outside of their Context, a Role Method only has
# access to its associated object while the role's context is the
# current/active global context.
# - Following Trygve's approach: "Roles are referenced by name. If I mean
# the Role I write the role name, not 'self'."
# - For referencing the object playing a role, I've overridden the Role's
# method_missing to forward methods to its associated object (Role Methods
# get precedence over the object's instance methods). I'm not so keen on
# this, so when calling object instance methods I've chosen to use the
# boilerplate role method "player" in preference of self or Ledgers (the Role
# class in the example) to retrieve the role player object from the context
# before we would hit the Role's method_missing. But it could be left to the
# programmer's style whether to use Role, self or player when sending
# messages to the role player.
# - Another thing was the need to avoid passing a Role instead of its
# associated object as an argument to outside of the current context. The
# MoneyTransfer context passes an amount argument to the Account context
# method decrease_balance(), so I had to make sure in the MoneyTransfer
# context the associated amount object and not the Amount role class was
# passed (roles don't have meaning outside their context).
# While I like the inline readability of the contexts here, and the lack of
# code injection into domain objects, I'm not sure how acceptable the
# boilerplate code is and also the need to explicitly set the active context
# within every context method. I'd be very interested to hear any feedback on
# this; on whether the approach here can be approved upon or on whether role
# injection is ultimately preferable for DCI in ruby.
#
#
-----------------------------------------
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