Chain of responsibility in Ruby
A simple but useful implemenation of the chain of responsibility pattern in Ruby.
Chain of responsibility
The chain of responsibility pattern is a "design pattern consisting of a source of command objects and a series of processing objects.
Each processing object contains a set of logic that describes the types of command objects that it can handle, and how to pass off those that it cannot to the next processing object in the chain. A mechanism also exists for adding new processing objects to the end of this chain.
In other words, it's a real useful pattern whenever you're coding command objects which may need to operate in succession. I recently came across this need on my Juicer project, where I have a set of file commands; merge files, minify files, add cache busters, and so on. These command objects all operate on a single file, and the chain of responsibility offers a clean way to add operations to be executed on a file.
In Juicer, I also make use of the abort possibility. For JavaScript files, the first command in the chain is the one that uses JsLint to verify the correctness of a script. If this fails the chain is broken.
Implementation
For this implementation I chose arbitrary objects to be "the processing objects", and methods on these objects the actual commands.
All you really need to implement the chain of responsibility pattern is:
- A pointer to the next object in the chain
- A way of aborting (ie, stopping the rest of the chain from executing)
- A call for the next command to execute in each command
In order to avoid duplicating this boiler-plate code in every command class (or some super class), I pulled this into a module you can include in any class you want to treat as a processor. You can then use the chain_method "macro"/method to say which methods a given class chains.
Once you've included the Chainable module, objects will also have a set_next/ next_in_chain= method that you can use to set the next command in chain. Note that there is no way to build individual chains for specific methods on the same object (ie one chain for method1 and another for method2).
In a nutshell:
class MinifyCommand
include Chainable
def execute(file)
# ...
end
chain_method :execute
end
Note that you cannot call chain_method before the actual method is defined.
Example: loggers
I think this will all be clearer by way of an example. The following example was shamelessly stolen from Wikipedia, and slightly modified. It shows how the chain of responsibility pattern can be used to set up a logging chain and control how deep into this chain a given message travels:
# Example is a simplified version of the Wikipedia one
# (http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern)
#
class Logger
include Chainable
ERR = 3
NOTICE = 5
DEBUG = 7
def initialize(level)
@level = level
end
def log(str, level)
if level <= @level
write str
else
abort_chain
end
end
def write(str)
puts str
end
chain_method :write
end
class EmailLogger < Logger
def write(str)
p "Logging by email"
# ...
end
end
logger = Logger.new(Logger::NOTICE)
logger.next_in_chain = EmailLogger.new(Logger::ERR)
logger.log("Some message", Logger::DEBUG) # Ignored
logger.log("A warning", Logger::NOTICE) # Logged to console
logger.log("An error", Logger::ERR) # Logged to console and emai
The implementation itself is fairly simple. It's listed below, but you can also get it from Github.
module Chainable
#
# Add the chain_method to classes that includes the module
#
def self.included(base)
base.extend(ClassMethods)
end
#
# Sets the next command in the chain
#
def next_in_chain=(next_obj)
@_next_in_chain = next_obj
next_obj || self
end
alias_method :set_next, :next_in_chain=
#
# Get next command in chain
#
def next_in_chain
@_next_in_chain ||= nil
@_next_in_chain
end
private
#
# Abort the chain for the current message
#
def abort_chain
@_abort_chain = true
end
module ClassMethods
#
# Sets up a method for chaining
#
def chain_method(method)
original_method = "execute_#{method}".to_sym
alias_method original_method, method
self.class_eval <<-RUBY
def #{method}(*args, &block)
@_abort_chain = false
#{original_method}(*args, &block)
next_in_chain.#{method}(*args, &block) if !@_abort_chain && next_in_chain
end
RUBY
end
end
end
Comments
Onno
5. August, 15:57
Christian
9. August, 23:14
Comments are closed