Pattern Overview
The Chain of Responsibility (CoR) pattern is a behavioral design pattern that promotes loose coupling between a sender and a receiver. It accomplishes this by allowing a request to traverse a chain of potential handler objects. Each object in the chain is given an opportunity to process the request, either handling it completely or passing it along to the next node in the sequence. This process continues until a handler processes the request or the end of the chain is reached.
Applicable Scenarios
This pattern is particularly useful in systems requiring flexible request routing or processing pipelines. Common use cases include:
- Workflow Automation: Multi-level approval systems where a request moves from a team lead to a manager and finally to a director.
- Event Handling: Game logic where an event (like a collision) is checked by various subsystems (physics, scoring, sound) in a specific order.
- Middleware Systems: Web server filters that pre-process or post-process HTTP requests and responses.
CoR is ideal when the set of handlers should be defined dynamically, or when the specific handler for a request must be determined at runtime without the client needing to know the specific logic.
Architecture and Roles
The architecture typically consists of two primary roles:
- Abstract Handler: Defines an interface for handling requests and maintains a reference to the next handler in the chain (the successor).
- Concrete Handler: Implements the handling logic. If it can process the request, it does so; otherwise, it forwards the request to its successor.
This structure decouples the request sender from the specific processing logic, allowing the chain to be modified or extended independently.
Framework Implementations
Servlet Filters (Java EE / Jakarta EE)
In the Java Servlet specification, the Filter mechanism is a classic implementation of CoR. The FilterChain object manages the order of execution.
public interface Filter {
void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException;
}
public interface FilterChain {
void doFilter(ServletRequest req, ServletResponse res)
throws IOException, ServletException;
}Frameworks like Spring provide concrete implementations to manage these chains. Below is a simplified logic often used to iterate through a list of filters:
public class SimpleFilterChain implements FilterChain {
private final List<Filter> filters;
private int currentPosition = 0;
public SimpleFilterChain(List<Filter> filters) {
this.filters = new ArrayList<>(filters);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res)
throws IOException, ServletException {
if (currentPosition < filters.size()) {
Filter next = filters.get(currentPosition);
currentPosition++;
next.doFilter(req, res, this);
}
}
}Netty ChannelPipeline
Netty utilizes a sophisticated version of this pattern for its network data processing. The ChannelPipeline manages a doubly linked list of ChannelHandler objects. Data flows through the pipeline, allowing different handlers to process inbound (incoming) and outbound (outgoing) data.
public class DefaultChannelPipeline {
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
public DefaultChannelPipeline(Channel channel) {
// Tail and Head are sentinel nodes handling context lifecycle
tail = new TailContext(this);
head = new HeadContext(this);
// Linking the doubly linked list
head.next = tail;
tail.prev = head;
}
public void addLast(String name, ChannelHandler handler) {
AbstractChannelHandlerContext newCtx = new DefaultChannelHandlerContext(this, name, handler);
// Logic to insert newCtx before the tail
newCtx.prev = tail.prev;
newCtx.next = tail;
tail.prev.next = newCtx;
tail.prev = newCtx;
}
}In Netty, a request propagates from the head to the tail. Any handler can interrupt the propagation by simply not calling the next handler, effectively terminating the chain.
Merits and Drawbacks
Advantages
- Decoupling: The sender does not need to know which object will handle the request.
- Flexibility: The chain structure can be modified dynamically at runtime by adding or removing handlers.
- Single Responsibility: Each handler focuses on a specific task, promoting cleaner code.
- Open/Closed Principle: New handlers can be introduced without modifying existing client code.
Disadvantages
- Performance: Long chains can introduce latency as the request traverses multiple objects.
- Debugging Difficulty: The flow of execution can be hard to trace, especially if the chain is dynamic or handlers are silent.
- Risk of Cycles: Improper implementation (e.g., circular references in the chain) can lead to infinite loops.