Skip to content

Instantly share code, notes, and snippets.

@jimsynz
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimsynz/8965588 to your computer and use it in GitHub Desktop.
Save jimsynz/8965588 to your computer and use it in GitHub Desktop.
So, after our dopodcast episode about DCI (http://dopodcast.org/blog/2014/02/10/show-9-dci-with-jim-gay-and-craig-ambrose/) I've been thinking a lot about the intersection of DCI and Promises/A+ in Ruby, and here's an example of what I've been thinking.
class MrDarcy
class Context
class << self
def role role_name, &block
role_name = role_name.to_sym
roles[role_name] = Module.new(&block)
role_name
end
def action action_name, &block
define_method action_name.to_sym do |*args|
self.then(*args, &block)
end
end
def roles
@roles ||= {}
end
def new role_players={}
context = super
ObjectSpace.define_finalizer context, finializer(role_players)
context
end
private
def finializer(role_players={})
proc do
roles.each do |role_name, mod|
player = role_players[role_name]
mod.instance_methods.each do |method_name|
player.singleton_class.send :remove_method, method_name
end
end
end
end
end
attr_accessor :result
def initialize role_players={}
roles = self.class.roles
roles.each do |role_name, mod|
player = role_players[role_name]
raise ArgumentError, "No role player for #{role_name} supplied" unless player
extend_player_with_role(player, mod)
self.singleton_class.send :define_method, role_name do
player
end
end
end
# This isn't really a promise implementation, it's just a simple
# demonstration of the ideas. Real promises are async, etc.
def then *args, &block
do_thenable(*args, &block) unless result.is_a? Exception
self
end
def fail &block
if result.is_a? Exception
do_thenable(&block)
self.result = nil
end
self
end
private
def do_thenable *args, &block
args = args.any? ? args : [result]
begin
self.result = self.instance_exec(*args, &block)
rescue Exception => e
self.result = e
end
end
# `extend_player_with_role` defines each role's method on the object
# instance's eigenclass so that the finalizer can remove it again
# afterwards.
# This is because we can't unextend a module, once it's mixed in,
# otherwise I would just do that.
def extend_player_with_role player, mod
mod.instance_methods.each do |method_name|
implementation = mod.instance_method method_name
player.singleton_class.send :define_method, method_name, implementation
end
end
end
end
## EXAMPLES START HERE:
class TransferFunds < MrDarcy::Context
role :money_source do
def has_available_funds? amount
available_balance >= amount
end
def subtract_funds amount
self.available_balance = available_balance - amount
end
end
role :money_destination do
def receive_funds amount
self.available_balance = available_balance + amount
end
end
action :transfer do |amount|
if money_source.has_available_funds? amount
money_source.subtract_funds amount
money_destination.receive_funds amount
else
raise "insufficient funds"
end
amount
end
end
Account = Struct.new(:available_balance, :account_number)
successful_context = TransferFunds.new \
money_source: Account.new(15, :A),
money_destination: Account.new(13, :B)
puts "Transferring 5 from A=15 to B=13"
successful_context.transfer(5).then do |amount|
puts "Successfully transferred #{amount} from #{money_source.account_number} to #{money_destination.account_number}"
puts "Source balance now: #{money_source.available_balance}"
puts "Destination balance now: #{money_destination.available_balance}"
end
puts
puts "Transferring 50 from A=1, B=20"
failing_context = TransferFunds.new \
money_source: Account.new(1, :A),
money_destination: Account.new(20, :B)
failing_context.transfer(50).then do |amount|
puts "Successfully transferred #{amount} from #{money_source.account_number} to #{money_destination.account_number}"
puts "Source balance now: #{money_source.available_balance}"
puts "Destination balance now: #{money_destination.available_balance}"
end.fail do |e|
puts "Failed to transfer from #{money_source.account_number} to #{money_destination.account_number}: #{e.message}"
end
puts
puts "Transferring 10 then 3 from A=100 to B=15"
chained_context = TransferFunds.new \
money_source: Account.new(100, :A),
money_destination: Account.new(15, :B)
chained_context.transfer(10).transfer(3).then do |amount|
puts "Successfully transferred #{amount} from #{money_source.account_number} to #{money_destination.account_number}"
puts "Source balance now: #{money_source.available_balance}"
puts "Destination balance now: #{money_destination.available_balance}"
end
puts
printf "Ensuring roles are cleaned off role players: "
source = Account.new(10, :A)
dest = Account.new(0, :B)
TransferFunds.new(money_source: source, money_destination: dest).transfer(5)
msgs = []
[:has_available_funds?, :subtract_funds].each do |m|
msgs << "Source responds to #{m}" unless source.respond_to? m
end
msgs << "Destination responds to receive_funds" unless dest.respond_to? :receive_funds
if msgs.empty?
puts "Yup."
else
puts "Nope:\n"
puts msgs.join("\n")
end
Transferring 5 from A=15 to B=13
Successfully transferred 5 from A to B
Source balance now: 10
Destination balance now: 18
Transferring 50 from A=1, B=20
Failed to transfer from A to B: insufficient funds
Transferring 10 then 3 from A=100 to B=15
Successfully transferred 3 from A to B
Source balance now: 87
Destination balance now: 28
Ensuring roles are cleaned off role players: Yup.
@jimsynz
Copy link
Author

jimsynz commented Feb 12, 2014

Would be interesting for the role method to take options like:

role :money_source, must_respond_to: :available_balance do
  # ...
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment