Last active
August 29, 2015 13:56
-
-
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.
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
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 |
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
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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Would be interesting for the
role
method to take options like: