Java Design Patterns — Chain Of Responsibility
The Chain of Responsibility (CoR) is a behavioral design pattern used to handle a sequence of objects, allowing multiple objects to process a request without coupling the sender to a specific receiver. The key idea is to pass a request along a chain of handlers, where each handler decides whether to process the request or pass it to the next handler in the chain.
This design pattern is beneficial in situations where multiple objects might handle a request, but you don’t want to hard-code the relationships between these objects. Instead, you create a chain dynamically at runtime and let the objects decide if they can handle the request or delegate it to the next one.
Key Concepts
- Handler Interface: This defines the contract for handling the request. It typically contains a method for processing the request and a reference to the next handler in the chain.
- Concrete Handlers: These classes implement the handler interface. Each handler is responsible for processing a certain type of request or passing the request to the next handler in the chain.
- Client: The client sends the request to the first handler in the chain. From there, the request propagates along the chain until it is handled.
Example Scenario: Support Request Handling
Imagine you have a technical support system where a request can either be handled by Level 1 support, Level 2 support, or escalated to a manager.
Step-by-Step Implementation
- Handler Interface
public abstract class SupportHandler {
protected SupportHandler nextHandler;
public void setNextHandler(SupportHandler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract void handleRequest(SupportRequest request);
}
The SupportHandler
is an abstract class that defines the handleRequest
method and provides a reference to the next handler in the chain.
2. Concrete Handlers
public class Level1Support extends SupportHandler {
@Override
public void handleRequest(SupportRequest request) {
if (request.getSeverity() == 1) {
System.out.println("Level 1 support handled the request: " + request.getMessage());
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
public class Level2Support extends SupportHandler {
@Override
public void handleRequest(SupportRequest request) {
if (request.getSeverity() == 2) {
System.out.println("Level 2 support handled the request: " + request.getMessage());
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
public class ManagerSupport extends SupportHandler {
@Override
public void handleRequest(SupportRequest request) {
if (request.getSeverity() == 3) {
System.out.println("Manager handled the request: " + request.getMessage());
} else {
System.out.println("Request not handled.");
}
}
}
In this implementation:
- Level1Support handles requests with a severity of 1.
- Level2Support handles requests with a severity of 2.
- ManagerSupport handles the most severe requests (severity 3).
3. Support Request Class
public class Level1Support extends SupportHandler {
public class SupportRequest {
private int severity;
private String message;
public SupportRequest(int severity, String message) {
this.severity = severity;
this.message = message;
}
public int getSeverity() {
return severity;
}
public String getMessage() {
return message;
}
}
The SupportRequest
class represents the request that will be processed. It contains details like the severity level and message.
4. Client Code
public class SupportClient {
public static void main(String[] args) {
// Create handlers
SupportHandler level1 = new Level1Support();
SupportHandler level2 = new Level2Support();
SupportHandler manager = new ManagerSupport();
// Set the chain of responsibility
level1.setNextHandler(level2);
level2.setNextHandler(manager);
// Create requests
SupportRequest request1 = new SupportRequest(1, "Basic issue.");
SupportRequest request2 = new SupportRequest(2, "Advanced issue.");
SupportRequest request3 = new SupportRequest(3, "Critical issue.");
SupportRequest request4 = new SupportRequest(4, "Unknown issue.");
// Pass the requests to the chain
level1.handleRequest(request1);
level1.handleRequest(request2);
level1.handleRequest(request3);
level1.handleRequest(request4);
}
}
Output:
Level 1 support handled the request: Basic issue.
Level 2 support handled the request: Advanced issue.
Manager handled the request: Critical issue.
Request not handled.
Explanation
- The client sends various requests to the first handler in the chain, which is
Level1Support
. IfLevel1Support
cannot handle the request, it delegates the task toLevel2Support
, and so on until the request is either handled or no handler is left in the chain. - For requests that don’t match any handler’s criteria, the chain ends without handling them.
Advantages of the Chain of Responsibility Pattern
- Decoupling the Sender and Receiver: The sender of the request (the client) doesn’t know which object in the chain will handle the request.
- Flexibility: The chain can be modified dynamically at runtime, allowing you to add or remove handlers without affecting the client.
- Single Responsibility Principle: Each handler has only one specific responsibility, making the code easier to maintain and extend.
Drawbacks
- Uncertainty of Request Handling: There’s no guarantee that the request will be handled by any handler, which can lead to unhandled requests unless a fallback mechanism is in place.
- Performance Issues: The request might pass through multiple handlers before it is processed, leading to inefficiencies if the chain is too long or complex.
When to Use the Chain of Responsibility Pattern
- You need to process a request by more than one object.
- The set of handlers should be flexible and can change at runtime.
- You want to decouple the sender of the request from the receivers.
Conclusion
The Chain of Responsibility pattern is a powerful design pattern that promotes flexibility and decoupling in request processing. By passing requests along a chain of handlers, you ensure that the system is both extensible and maintainable, as you can modify the chain without impacting the overall structure. This makes it a useful tool for scenarios where multiple objects could potentially handle a request in different ways.