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

emitting_event_identifier(action_klass) click to toggle source

@private

# File lib/emittance/action.rb, line 141
def emitting_event_identifier(action_klass)
  Emittance::Event.event_klass_for action_klass
end
find_or_create_klass(klass_name) click to toggle source

@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
handler_klass_name(action_klass) click to toggle source

@private

# File lib/emittance/action.rb, line 136
def handler_klass_name(action_klass)
  "#{action_klass}Handler"
end
included(action_klass) click to toggle source
# 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

action_klass_blk() click to toggle source

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
handler_klass_blk(action_klass) click to toggle source
# 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
setup_action_klass(action_klass) click to toggle source

Class setups

# File lib/emittance/action.rb, line 156
def setup_action_klass(action_klass)
  action_klass.class_eval(&action_klass_blk)
end
setup_handler_klass(handler_klass, action_klass) click to toggle source
# 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