public
Last active

Playing with DCI in Ruby: Chaining Contexts

  • Download Gist
README.md
Markdown

DCI: Chain of contexts example

Run each chain:

$ ruby full_chain.rb
$ ruby half_chain.rb
$ ruby no_chain.rb
contexts.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
# encoding: utf-8
 
# A Data, Context, Interaction (DCI) experiment.
# Here we will try to create a DSL to manipulate contexts.
 
class Context
 
# Add all the liteners. A listener is an Object that
# responds to the methods `#success` and `#failure`.
def initialize(listeners)
@listeners = listeners
@events = []
end
 
# Executes the use case. When finish, the method `#publish` should
# be called to notify to all the listeners.
#
# The @params parameters is a hash with all the information
# that the context needs. Is passed to each listener.
def call(params)
# Extract params, instances and initiate the use case
raise 'Method not implemented'
end
 
# Notify to all the listeners the result of the context
# execution.
def publish(event, params, keywords = [])
@listeners.map do |listener|
listener.send(event, params, keywords)
end
end
 
# In order to be able to chain contexts, a context must
# act as a listener, thats why it responds to `#success`
# and `#failure` methods.
 
# By default, a context is not listening to any event.
# The context initiator should indicates the events that each
# context should be aware of.
def listen(event)
@events << event.to_sym
end
 
def success(params, keywords = [])
trigger(:success, params, keywords)
end
 
def failure(params, keywords = [])
trigger(:failure, params, keywords)
end
 
# If the context is listening to the event, it calls himself.
# If not, pass the call to its own listeners, following the chain.
def trigger(event, params, keywords = [])
if @events.include?(event)
# TODO: save keywords in a history
call(params.dup)
else
publish(event, params.dup, keywords)
end
end
end
 
class RegisterUser < Context
def call(params)
user = UserData.new(params['user_params'])
user.extend GuestUser
 
_params = params.dup
_params['user'] = user
if user.signup
publish(:success, _params, ['register_user'])
else
publish(:failure, _params, ['register_user'])
end
end
end
 
class SavePendingPurchase < Context
def call(params)
pending_purchase = PendingPurchaseData.new(params['pending_purchase_params'])
pending_purchase.extend NewPendingPurchase
 
_params = params.dup
_params['pending_purchase'] = pending_purchase
 
if pending_purchase.save_for_user(_params['user'])
publish(:success, _params, ['save_pending_purchase'])
else
publish(:failure, _params, ['save_pending_purchase'])
end
end
end
data.rb
Ruby
1 2 3 4 5 6 7 8 9
# encoding: utf-8
 
# Dummy data models
 
class UserData < Struct.new(:params)
end
 
class PendingPurchaseData < Struct.new(:params)
end
full_chain.rb
Ruby
1 2 3 4 5 6 7 8 9 10
# encoding: utf-8
 
require './initiator'
 
params = {
'user_params' => { 'success' => true },
'pending_purchase_params' => { 'success' => true }
}
 
Initiator.call(params)
half_chain.rb
Ruby
1 2 3 4 5 6 7 8 9 10
# encoding: utf-8
 
require './initiator'
 
params = {
'user_params' => { 'success' => true },
'pending_purchase_params' => { 'success' => false }
}
 
Initiator.call(params)
initiator.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
# encoding: utf-8
 
require './responder'
require './data'
require './roles'
require './contexts'
 
# Test class that executes a chain of contexts,
# showing how to use them.
class Initiator
def self.call(params)
new.call(params)
end
 
def call(params)
# Initialize the last element of the chain, the final responder.
responder = Responder.new
 
# Then, initialize each context of the chain.
# The last one to initialize is the first context to call.
# Each context will be a listener of the next one.
 
# SavePendingPurchase is the last context to be executed, thats why
# it has the responder as a listener.
save_pending_purchase = SavePendingPurchase.new([responder])
# Only acts if the previous context was successful.
save_pending_purchase.listen(:success)
 
# RegisterUser is the first context, so it has the SavePendingPurchase
# as a listener.
register_user = RegisterUser.new([save_pending_purchase])
 
register_user.call(params)
end
end
no_chain.rb
Ruby
1 2 3 4 5 6 7 8 9 10
# encoding: utf-8
 
require './initiator'
 
params = {
'user_params' => { 'success' => false },
'pending_purchase_params' => { 'success' => true }
}
 
Initiator.call(params)
responder.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# encoding: utf-8
 
# Final listener. Shows who was the last caller.
class Responder
def success(params, keywords = [])
notify('success', keywords)
end
 
def failure(params, keywords = [])
notify('failure', keywords)
end
 
def notify(event, keywords)
message = "Context: #{keywords.join(', ')}"
 
$stdout.puts "[#{event.upcase}] #{message}"
end
end
roles.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
# encoding: utf-8
 
# Dummy roles, only for testing purposes.
 
module Notify
def notify(label, message)
$stdout.puts "[#{label.upcase}] #{message}"
end
end
 
module GuestUser
include Notify
 
def signup
if params.fetch('success', false)
notify('signup', 'User CAN signup')
true
else
notify('signup', 'User CANNOT signup')
false
end
end
end
 
module NewPendingPurchase
include Notify
 
def save_for_user(user)
if params.fetch('success', false)
notify('pending_purchase', 'Pending purchase CAN be saved')
true
else
notify('pending_purchase', 'Pending purchase CANNOT be saved')
false
end
end
end

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.