module Emittance::Action
Consider the usual “Service Object” pattern:
class Foo def assign FooAssignment.new(self).call end end class FooAssignment attr_reader :assignable def initialize(assignable) @assignable = assignable end def call do_stuff end # ... end
There are variations on this pattern, the idea is that the service object represents something that your application is doing. However, this can easily just become a proxy for the same antipattern it was made to solve. We might wind up with a #call
method like the following:
class FooAssignment # ... def call do_stuff do_stuff_to_another_object do_stuff_to_something_else do_stuff_to_yet_another_thing end # ... end
We can use the Emittance
core features to prune those method calls:
class FooAssignment extend Emittance::Emitter # ... def call do_stuff emit :foo_assignment end # ... end
Emittance::Action
provides a shortcut for this. Just mix it in and implement #call
! This allows us to keep the expressitivity that a Service Object is made to provide, while preventing us from having to give such an object too many responsibilities.
Usage¶ ↑
First, define a class and include this module:
class FooAssignment include Emittance::Action attr_reader :assignable def initialize(assignable) @assignable = assignable end end
Next, we'll implement the #call
instance method. Emittance::Action
will take care of the dirty work for us:
class FooAssignment # ... def call do_one_and_i_mean_only_one_thing end # ... end
From here, your code should be able to run without error! You might notice, though, that a mysterious class will have been defined after loading this file.
defined? FooAssignmentHandler => "constant"
Next, we can open up this class to implement the event handler. Emittance
will look for a method called #handle_call
, and invoke it whenever, in this example, +FooAssignment#call+ is called.
class FooAssignmentHandler def handle_call notify_someone(action) end # ... end
The “Action” object is stored as the instance variable +@action+, made available with a getter class #action
. This will allow us to access its data and make decisions based on it.
Now, this seems like we're passing the buck of all that control flow to yet another object, but this pattern has several advantages. First, we can disable Emittance
at will, so if we ever want to shut FooAssignment
actions off from their listeners, that is always an option to us. Second, to address the concern raised at the beginning of this paragraph, this paradigm puts us into the mindset of spreading the flow of our program out across multiple action/handler pairs, allowing us to think more clearly about what our code is doing.
One possible disadvantage of this pattern is that it suggests a one-to-one pairing between events and handlers.
Constants
- EMITTING_METHOD
Name of the method that will emit an event when invoked.
- HANDLER_METHOD_NAME
Name of the method that will be invoked when the handler class captures an event.
Public Class Methods
@private
# File lib/emittance/action.rb, line 141 def emitting_event_identifier(action_klass) Emittance::Event.event_klass_for action_klass end
@private
# File lib/emittance/action.rb, line 146 def find_or_create_klass(klass_name) set_namespaced_constant_by_name(klass_name, Class.new) unless Object.const_defined?(klass_name) Object.const_get klass_name end
@private
# File lib/emittance/action.rb, line 136 def handler_klass_name(action_klass) "#{action_klass}Handler" end
# File lib/emittance/action.rb, line 127 def included(action_klass) handler_klass_name = Emittance::Action.handler_klass_name(action_klass) handler_klass = Emittance::Action.find_or_create_klass(handler_klass_name) setup_action_klass action_klass setup_handler_klass handler_klass, action_klass end
Private Class Methods
rubocop:disable Metrics/MethodLength
# File lib/emittance/action.rb, line 167 def action_klass_blk lambda do |_klass| extend Emittance::Emitter class << self define_method :method_added do |method_name| emitting_method = Emittance::Action::EMITTING_METHOD identifier = Emittance::Action.emitting_event_identifier(self) emits_on(method_name, identifier: identifier) if method_name == emitting_method super method_name end end end end
# File lib/emittance/action.rb, line 182 def handler_klass_blk(action_klass) lambda do |_klass| attr_reader :action extend Emittance::Watcher define_method :initialize do |action_obj| @action = action_obj end watch Emittance::Action.emitting_event_identifier(action_klass) do |event| handler_obj = new(event.emitter) handler_method_name = Emittance::Action::HANDLER_METHOD_NAME handler_obj.send(handler_method_name) if handler_obj.respond_to?(handler_method_name) end end end
Class setups
# File lib/emittance/action.rb, line 156 def setup_action_klass(action_klass) action_klass.class_eval(&action_klass_blk) end
# File lib/emittance/action.rb, line 160 def setup_handler_klass(handler_klass, action_klass) handler_klass.class_eval(&handler_klass_blk(action_klass)) end