Chain of Responsibility

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

Participants

RoleIn this exampleResponsibility
Handler#handle(key)The interface every link in the chain shares.
Concrete HandlerQuitKey, GameOverScreen, PlayControlsHandles the keys it cares about and passes the rest to its successor.
Terminal HandlerUnboundKeyThe last link. It always answers, so nothing falls off the end.
ClientCaller ofSends each key to the first link without knowing who will handle it.

Ruby implementation

chain_of_responsibility.rb
# Handlers for a key press. Each one either deals with the key or passes it
# on, so overlays like "game over" get first say without an `if` in every
# other handler.
class QuitKey
  def initialize(successor)
    @successor = successor
  end

  def handle(key)
    return "Quit" if key == "q"

    @successor.handle(key)
  end
end

class GameOverScreen
  def initialize(successor, over:)
    @successor = successor
    @over = over
  end

  # While the game is over, this handler swallows every key.
  def handle(key)
    return @successor.handle(key) unless @over

    key == "r" ? "New game" : "Game over: press r to play again"
  end
end

class PlayControls
  ACTIONS = { "n" => "Next wave", "u" => "Upgrade tower", "s" => "Sell tower" }.freeze

  def initialize(successor)
    @successor = successor
  end

  def handle(key)
    ACTIONS.fetch(key) { @successor.handle(key) }
  end
end

# The end of the chain. It always answers, so no handler ever has to check
# whether it has a successor.
class UnboundKey
  def handle(key)
    "Nothing is bound to #{key.inspect}"
  end
end

In use

chain = QuitKey.new(
          GameOverScreen.new(
            PlayControls.new(UnboundKey.new),
            over: false))

chain.handle("n")  # => "Next wave"
chain.handle("x")  # => "Nothing is bound to \"x\""
chain.handle("q")  # => "Quit"

Build the chain with over: true and GameOverScreen swallows every key except q (handled before it) and r. Play controls never need an if game.over? check.

Notes

Ruby idioms

  • End the chain with an object, not nil. UnboundKey always answers, so no handler needs an if @successor check. It’s a Null Object that also does useful work.
  • Hash#fetch with a block reads naturally as “handle it if I know it, otherwise pass it on”: ACTIONS.fetch(key) { @successor.handle(key) }.
  • Rack middleware is a chain. Each middleware handles the request or calls @app.call(env) to pass it on.

Reach for it when

  • More than one object might handle a request, and which one should be decided at runtime: input handling with overlays, approval rules, error handlers.
  • You want to add, remove or reorder handlers without changing the sender.

Watch for

  • The order of the chain is behaviour. Here QuitKey comes before GameOverScreen, which is why q still works after the game ends.
  • Long chains are hard to debug. Have handlers say what happened, as these strings do.

Each link either handles the request or passes it on.