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
| Role | In this example | Responsibility |
|---|---|---|
| Handler | #handle(key) | The interface every link in the chain shares. |
| Concrete Handler | QuitKey, GameOverScreen, PlayControls | Handles the keys it cares about and passes the rest to its successor. |
| Terminal Handler | UnboundKey | The last link. It always answers, so nothing falls off the end. |
| Client | Caller of | Sends each key to the first link without knowing who will handle it. |
Ruby implementation
# 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
endIn 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.UnboundKeyalways answers, so no handler needs anif @successorcheck. It’s a Null Object that also does useful work. Hash#fetchwith 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
QuitKeycomes beforeGameOverScreen, which is whyqstill 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.