Chain of Responsibility
Before you begin: Objects, references, conditions, and nullable results.
Chain of Responsibility passes a request through potential handlers until one handles it. The sender starts the request at the beginning of the chain rather than selecting the final handler itself. 1
Ask for help without searching every component yourself
A game screen can answer questions about chat controls or movement. Imagine that help starts at the focused component, then passes to a broader component if the first has no answer.
Each node below knows one topic and optionally another node. It either answers or forwards. A visit counter makes it possible to see which nodes were consulted; it is diagnostic data, not a requirement of the pattern.
public sealed class HelpNode
{
private readonly string topic;
private readonly string answer;
private readonly HelpNode? next;
public int Visits { get; private set; }
public HelpNode(string topic, string answer, HelpNode? next = null)
{
this.topic = topic;
this.answer = answer;
this.next = next;
}
public string? Find(string requestedTopic)
{
Visits++;
if (requestedTopic == topic) return answer;
return next?.Find(requestedTopic);
}
}var movement = new HelpNode("move", "Use arrows");
var chat = new HelpNode("chat", "Press Enter", movement);
Console.WriteLine(chat.Find("move")); // Use arrows
Console.WriteLine(chat.Find("unknown") ?? "No help available");The chat node cannot answer the movement request, so it forwards it. The movement node answers. A chat request would stop at the first node; the second node would not be visited.
class HelpNode:
def __init__(self, topic, answer, next_node=None):
self._topic = topic
self._answer = answer
self._next = next_node
self.visits = 0
def find(self, requested_topic):
self.visits += 1
if requested_topic == self._topic:
return self._answer
if self._next is not None:
return self._next.find(requested_topic)
return Nonemovement = HelpNode("move", "Use arrows")
chat = HelpNode("chat", "Press Enter", movement)
print(chat.find("move")) # Use arrows
answer = chat.find("unknown")
print("No help available" if answer is None else answer)The first matching node returns an answer. Reaching the end without a match returns None, which the caller handles explicitly instead of assuming every request succeeds.
Order is a policy
If two nodes can answer the same topic, the earlier one wins. Reordering handlers can therefore change behavior. A more specific handler commonly comes before a general fallback; putting an unconditional fallback first would make later handlers unreachable.
The nodes happen to share one class here because their behavior differs only in configured values. More complicated handlers can have different implementations of a common handling operation. A class hierarchy is not necessary merely to make a chain.
A simple dictionary would be better for this fixed topic table. A chain becomes useful when handling depends on runtime context, handlers have different responsibilities, or the sequence must be assembled independently of the request sender.
Unlike ordinary Observer notification, this example does not notify everyone. Unlike a pipeline in which every stage transforms a value, it stops after one handler accepts responsibility. Middleware sometimes uses a related chain shape with different forwarding rules; state those rules rather than inferring them from the name.
Keep the chain acyclic and decide how failures propagate. A handler throwing an exception is not the same as declining a request. Silently trying the next handler after an authorization failure, for example, could change an enforced denial into a bypass.
Sources
- Gamma, Helm, Johnson, and Vlissides — Chain of Responsibility, the original catalog excerpt.