lib/emittance/action.rb in emittance-0.0.1 vs lib/emittance/action.rb in emittance-0.0.2
- old
+ new
@@ -1,199 +1,192 @@
-##
-# There are certain classes (ergo objects) that represent an action taken by another object. This pattern goes like so:
-#
-# class Foo
-# def assign
-# Assignment.new(self).call
-# end
-# end
-#
-# class Assignment
-# attr_reader :assignable
-#
-# def initialize(assignable)
-# @assignable = assignable
-# end
-#
-# def call
-# do_stuff
-# end
-#
-# # ...
-# end
-#
-# This pattern is useful for maintaining the single responsibility principle, delegating complex tasks to other objects
-# even when (in this particular case), it might be sensible for the +assign+ message to be sent to +Foo+. This has
-# numerous benefits, including the ability for actions like +Assignment+ to take a duck type.
-#
-# 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 Assignment
-# # ...
-#
-# def call
-# do_stuff
-# do_stuff_to_another_object
-# do_stuff_to_something_else
-# do_stuff_to_yet_another_thing
-# end
-#
-# # ...
-# end
-#
-# +Assignment+ is suddenly collaborating with a whole bunch of objects! This isn't bad in itself, but it might cause
-# some problems further on down the road as we add more responsibilities to +Assignment+. Do we really want all these
-# things to happen every time an +Assignment+ happens? If not, assuming this pattern we'll have to add a bunch of
-# control flow:
-#
-# class Assignment
-# # ...
-#
-# def call
-# do_stuff
-#
-# if some_condition
-# do_stuff_to_another_object
-#
-# if some_other_condition
-# do_stuff_to_something_else
-# else
-# do_stuff_to_yet_another_thing
-# end
-# elsif yet_another_condition
-# do_other_stuff_to_that_other_object
-# else
-# dont_actually_do_anything_but_notify_someone
-# end
-# end
-#
-# # ...
-# end
-#
-# This is obviously an extreme example (but not unheard of!), but it gets to the core of what this module tries to
-# solve. +Emittance::Action+ helps facilitate the single responsibility principle by emitting an event whenever we
-# invoke +#call+ on an object like +Assignment+.
-#
-# == Usage
-#
-# First, define a class and include this module:
-#
-# class Assignment
-# include Emittance::Action
-#
-# attr_reader :assignable
-#
-# def initialize(assignable)
-# @assignable = assignable
-# end
-# end
-#
-# Per the pattern explained above, instances of this class are representations of an action being carried out. This
-# class should have a very minimal interface (maybe, at most, some getter methods for its instance variables so the
-# handler can make decisions based on its state).
-#
-# Next, we'll implement the +#call+ instance method. +Emittance::Action+ will take care of the dirty work for us:
-#
-# class Assignment
-# # ...
-#
-# def call
-# do_one_and_i_mean_only_one_thing
-# end
-#
-# # ...
-# end
-#
-# Again, this method should do a single thing. 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? AssignmentHandler
-# => "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, +Assignment#call+ is called.
-#
-# class AssignmentHandler
-# 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 +Assignment+ 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.
-#
-module Emittance::Action
- EMITTING_METHOD = :call
- HANDLER_METHOD_NAME = "handle_#{EMITTING_METHOD}".to_sym
+# frozen_string_literal: true
- class NoHandlerMethodError < StandardError; end
+module Emittance
+ ##
+ # 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.
+ #
+ module Action
+ # Name of the method that will emit an event when invoked.
+ EMITTING_METHOD = :call
+ # Name of the method that will be invoked when the handler class captures an event.
+ HANDLER_METHOD_NAME = "handle_#{EMITTING_METHOD}".to_sym
- # @private
- class << self
- 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)
+ # @private
+ class << self
+ 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)
- action_klass.class_eval do
- extend Emittance::Emitter
+ action_klass.class_eval do
+ extend Emittance::Emitter
- class << self
- # @private
- def method_added(method_name)
- emitting_method = Emittance::Action::EMITTING_METHOD
- emits_on method_name if method_name == emitting_method
- super
+ class << self
+ # @private
+ def method_added(method_name)
+ emitting_method = Emittance::Action::EMITTING_METHOD
+ emits_on method_name if method_name == emitting_method
+ super
+ end
end
end
- end
- handler_klass.class_eval do
- attr_reader :action
+ handler_klass.class_eval do
+ attr_reader :action
- extend Emittance::Watcher
+ extend Emittance::Watcher
- def initialize(action_obj)
- @action = action_obj
- end
+ def initialize(action_obj)
+ @action = action_obj
+ end
- watch Emittance::Action.emitting_event_name(action_klass) do |event|
- handler_obj = new(event.emitter)
- handler_method_name = Emittance::Action::HANDLER_METHOD_NAME
+ watch Emittance::Action.emitting_event_name(action_klass) do |event|
+ handler_obj = new(event.emitter)
+ handler_method_name = Emittance::Action::HANDLER_METHOD_NAME
- if handler_obj.respond_to? handler_method_name
- handler_obj.send handler_method_name
+ if handler_obj.respond_to? handler_method_name
+ handler_obj.send handler_method_name
+ end
end
end
end
- end
- # @private
- def handler_klass_name(action_klass)
- "#{action_klass}Handler"
- end
+ # @private
+ def handler_klass_name(action_klass)
+ "#{action_klass}Handler"
+ end
- # @private
- def emitting_event_name(action_klass)
- Emittance::Emitter.emitting_method_event(action_klass, Emittance::Action::EMITTING_METHOD)
- end
+ # @private
+ def emitting_event_name(action_klass)
+ Emittance::Emitter.emitting_method_event(action_klass, Emittance::Action::EMITTING_METHOD)
+ end
- # @private
- def find_or_create_klass(klass_name)
- unless Object.const_defined? klass_name
- Object.const_set klass_name, Class.new(Object)
+ # @private
+ def find_or_create_klass(klass_name)
+ unless Object.const_defined? klass_name
+ set_namespaced_constant_by_name klass_name, Class.new
+ end
+
+ Object.const_get klass_name
end
- Object.const_get klass_name
+ private
+
+ def set_namespaced_constant_by_name(const_name, obj)
+ names = const_name.split('::')
+ names.shift if names.size > 1 && names.first.empty?
+
+ namespace = names.size == 1 ? Object : Object.const_get(names[0...-1].join('::'))
+ namespace.const_set names.last, obj
+ end
end
end
end