Skip to content

Instantly share code, notes, and snippets.

@sundbp
Created June 29, 2012 10:02
Show Gist options
  • Save sundbp/3017030 to your computer and use it in GitHub Desktop.
Save sundbp/3017030 to your computer and use it in GitHub Desktop.
DCI in ruby without injection (using DSL) - MoneyTransfer example
# 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