Created
June 29, 2012 10:02
-
-
Save sundbp/3017030 to your computer and use it in GitHub Desktop.
DCI in ruby without injection (using DSL) - MoneyTransfer example
This file contains 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
# Buidling blocks to support injectionless DCI | |
require 'rubygems' | |
require 'active_support/core_ext/string/inflections' | |
#require 'pry' | |
module ContextAccessor | |
def context | |
Thread.current[:context] | |
end | |
end | |
module Context | |
include ContextAccessor | |
module ClassMethods | |
include ContextAccessor | |
def create(*args, &block) | |
converted_args = args.map do |arg| | |
next arg if context.nil? | |
next context.player_for(arg) if context.has_player? arg | |
arg | |
end | |
new(*converted_args, &block) | |
end | |
def interaction(name, &block) | |
interaction_body = <<-EOF | |
def #{name}(*args) | |
execute_in_context do | |
block = self.class.interaction_block_for(:#{name}) | |
self.instance_exec(*args, &block) | |
end | |
end | |
EOF | |
class_eval interaction_body | |
register_interaction(name, &block) | |
end | |
def register_interaction(name, &block) | |
@interaction_blocks ||= Hash.new | |
@interaction_blocks[name] = block | |
end | |
def interaction_block_for(name) | |
@interaction_blocks ||= Hash.new | |
@interaction_blocks[name] | |
end | |
end | |
def self.included(klass) | |
klass.extend(ClassMethods) | |
klass.private_class_method :new | |
end | |
def role_player | |
@role_player ||= {} | |
@role_player | |
end | |
def assign_role(role, player) | |
role_player[role] = player | |
end | |
def player_for(role) | |
role_player[role] | |
end | |
def has_player?(role) | |
role_player.include? role | |
end | |
# 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 | |
return_object = begin | |
self.context = self | |
yield | |
ensure | |
self.context = old_context | |
end | |
return_object | |
end | |
end | |
# 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 | |
@my_context_class ||= self.to_s.chomp(role_name).constantize | |
@my_context_class | |
end | |
def role_name | |
@role_name ||= self.to_s.split("::").last | |
@role_name | |
end | |
def role_method(name, &block) | |
role_method_body = <<-EOF | |
def self.#{name}(*args) | |
converted_args = args.map do |arg| | |
next arg if context.nil? | |
next context.player_for(arg) if context.has_player? arg | |
arg | |
end | |
block = self.role_method_block_for(:#{name}) | |
self.instance_exec(*converted_args, &block) | |
end | |
EOF | |
class_eval role_method_body | |
register_role_method_block_for(name, &block) | |
end | |
def register_role_method_block_for(name, &block) | |
@role_method_blocks ||= Hash.new | |
@role_method_blocks[name] = block | |
end | |
def role_method_block_for(name) | |
@role_method_blocks ||= Hash.new | |
@role_method_blocks[name] | |
end | |
end | |
end | |
##################################################################### | |
# Contexts | |
class Account | |
include Context | |
def initialize(ledgers = []) | |
assign_role Ledgers, Array(ledgers) | |
end | |
interaction :balance do | |
Ledgers.balance | |
end | |
interaction :increase_balance do |amount| | |
Ledgers.add_entry 'depositing', amount | |
end | |
interaction :decrease_balance do |amount| | |
Ledgers.add_entry 'withdrawing', -1 * amount | |
end | |
# A role can use self or player to reference the obj associated with it | |
class Ledgers < Role | |
role_method :add_entry do |msg, amount| | |
player << LedgerEntry.new(msg, amount) | |
end | |
role_method :balance do | |
player.collect(&:amount).inject(0) {|sum, a| sum + a} | |
end | |
end | |
end | |
require 'logger' | |
class MoneyTransfer | |
include Context | |
def initialize(source, destination, amount) | |
assign_role Source, source | |
assign_role Destination, destination | |
assign_role Amount, amount | |
end | |
interaction :transfer do | |
Source.transfer Amount | |
end | |
class Source < Role | |
role_method :transfer do |amount| | |
log = Logger.new(STDOUT) | |
log.info "Source balance is #{Source.balance}" | |
log.info "Destination balance is #{Destination.balance}" | |
Destination.deposit amount and Source.withdraw amount | |
log.info "Source balance is now #{Source.balance}" | |
log.info "Destination balance is now #{Destination.balance}" | |
end | |
role_method :withdraw do |amount| | |
Source.decrease_balance amount | |
end | |
end | |
class Destination < Role | |
role_method :deposit do |amount| | |
Destination.increase_balance amount | |
end | |
end | |
class Amount < Role | |
end | |
end | |
# Data | |
class LedgerEntry | |
attr_accessor :amount | |
attr_accessor :message | |
def initialize(message, amount) | |
@message = message | |
@amount = amount | |
end | |
end | |
############################################################ | |
# Main | |
l1 = LedgerEntry.new('lodge', 500) | |
l2 = LedgerEntry.new('lodge', 420) | |
source = Account.create([l1,l2]) | |
destination = Account.create() | |
context = MoneyTransfer.create(source, destination, 700) | |
context.transfer |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment