Skip to content

cjohansen.no

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:

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

Possibly related

2006 - 2010 Christian Johansen Creative Commons License