The Chain of Responsibility (CoR) pattern is a behavioral design pattern that allows an object to send a command without knowing which object will handle the request. Instead of directly coupling the sender of a request to its receiver, the pattern chains the receiving objects and passes the request along the chain until an object handles it. This decouples the sender from the receiver and promotes flexibility and extensibility in handling requests.
Key Components
- Handler: An interface or abstract class defining a method to handle requests and a reference to the next handler in the chain.
- Concrete Handler: A class implementing the Handler interface, processing requests it can handle and forwarding others to the next handler.
- Client: Initiates the request and sends it to the first handler in the chain.
How It Works
- The Handler interface declares a method for processing requests and a way to set the next handler.
- Each Concrete Handler checks if it can handle the request based on certain conditions:
- If it can, it processes the request and may stop further propagation.
- If it cannot, it forwards the request to the next handler in the chain.
- The Client builds the chain (by linking handlers) and sends the request to the first handler.
- The request travels through the chain until it’s handled or reaches the end (if no handler processes it, a default action may occur).
Sample Implementation
Let’s model a support ticket system where tickets are processed by different levels of support (Level 1, Level 2, Level 3) based on their severity.
// Handler Interface
interface SupportHandler {
void handleRequest(Ticket ticket);
void setNextHandler(SupportHandler nextHandler);
}
// Ticket Class (Request)
class Ticket {
private String issue;
private int severity; // 1 = low, 2 = medium, 3 = high
public Ticket(String issue, int severity) {
this.issue = issue;
this.severity = severity;
}
public String getIssue() { return issue; }
public int getSeverity() { return severity; }
}
// Concrete Handlers
class Level1Support implements SupportHandler {
private SupportHandler nextHandler;
@Override
public void setNextHandler(SupportHandler nextHandler) {
this.nextHandler = nextHandler;
}
@Override
public void handleRequest(Ticket ticket) {
if (ticket.getSeverity() == 1) {
System.out.println("Level 1 Support handling: " + ticket.getIssue());
} else if (nextHandler != null) {
System.out.println("Level 1 Support passing ticket to next level...");
nextHandler.handleRequest(ticket);
} else {
System.out.println("No handler available for: " + ticket.getIssue());
}
}
}
class Level2Support implements SupportHandler {
private SupportHandler nextHandler;
@Override
public void setNextHandler(SupportHandler nextHandler) {
this.nextHandler = nextHandler;
}
@Override
public void handleRequest(Ticket ticket) {
if (ticket.getSeverity() == 2) {
System.out.println("Level 2 Support handling: " + ticket.getIssue());
} else if (nextHandler != null) {
System.out.println("Level 2 Support passing ticket to next level...");
nextHandler.handleRequest(ticket);
} else {
System.out.println("No handler available for: " + ticket.getIssue());
}
}
}
class Level3Support implements SupportHandler {
private SupportHandler nextHandler;
@Override
public void setNextHandler(SupportHandler nextHandler) {
this.nextHandler = nextHandler;
}
@Override
public void handleRequest(Ticket ticket) {
if (ticket.getSeverity() == 3) {
System.out.println("Level 3 Support handling: " + ticket.getIssue());
} else if (nextHandler != null) {
System.out.println("Level 3 Support passing ticket to next level...");
nextHandler.handleRequest(ticket);
} else {
System.out.println("No handler available for: " + ticket.getIssue());
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Create handlers
SupportHandler level1 = new Level1Support();
SupportHandler level2 = new Level2Support();
SupportHandler level3 = new Level3Support();
// Build the chain: Level 1 -> Level 2 -> Level 3
level1.setNextHandler(level2);
level2.setNextHandler(level3);
// Create tickets
Ticket ticket1 = new Ticket("Login issue", 1);
Ticket ticket2 = new Ticket("Database error", 2);
Ticket ticket3 = new Ticket("Server crash", 3);
Ticket ticket4 = new Ticket("Unknown issue", 4);
// Send tickets through the chain
System.out.println("Processing ticket 1:");
level1.handleRequest(ticket1);
System.out.println("\nProcessing ticket 2:");
level1.handleRequest(ticket2);
System.out.println("\nProcessing ticket 3:");
level1.handleRequest(ticket3);
System.out.println("\nProcessing ticket 4:");
level1.handleRequest(ticket4);
}
}
/*
Processing ticket 1:
Level 1 Support handling: Login issue
Processing ticket 2:
Level 1 Support passing ticket to next level...
Level 2 Support handling: Database error
Processing ticket 3:
Level 1 Support passing ticket to next level...
Level 2 Support passing ticket to next level...
Level 3 Support handling: Server crash
Processing ticket 4:
Level 1 Support passing ticket to next level...
Level 2 Support passing ticket to next level...
Level 3 Support passing ticket to next level...
No handler available for: Unknown issue
*/
Code language: PHP (php)
Use case and Implementation
Processing loan applications through a series of approval steps (e.g., initial review, risk assessment, final approval).
//LoanProcessingApplicationDemo.java // Step 1: Define the Handler interface interface LoanApprovalHandler { void setNext(LoanApprovalHandler handler); void processLoanApplication(double amount); } // Step 2: Implement the concrete handlers class InitialReviewHandler implements LoanApprovalHandler { private LoanApprovalHandler nextHandler; @Override public void setNext(LoanApprovalHandler handler) { this.nextHandler = handler; } @Override public void processLoanApplication(double amount) { if (amount <= 10000) { System.out.println("Initial Review: Loan approved for $" + amount); } else if (nextHandler != null) { nextHandler.processLoanApplication(amount); } } } class RiskAssessmentHandler implements LoanApprovalHandler { private LoanApprovalHandler nextHandler; @Override public void setNext(LoanApprovalHandler handler) { this.nextHandler = handler; } @Override public void processLoanApplication(double amount) { if (amount > 10000 && amount <= 50000) { System.out.println("Risk Assessment: Loan approved for $" + amount); } else if (nextHandler != null) { nextHandler.processLoanApplication(amount); } } } class FinalApprovalHandler implements LoanApprovalHandler { @Override public void setNext(LoanApprovalHandler handler) { // Final handler, no next handler to set } @Override public void processLoanApplication(double amount) { if (amount > 50000) { System.out.println("Final Approval: Loan approved for $" + amount); } else { System.out.println("Loan amount too low for final approval."); } } } // Step 3: Client code to demonstrate the Chain of Responsibility public class LoanProcessingApplicationDemo { public static void main(String[] args) { // Setup chain of responsibility LoanApprovalHandler initialReview = new InitialReviewHandler(); LoanApprovalHandler riskAssessment = new RiskAssessmentHandler(); LoanApprovalHandler finalApproval = new FinalApprovalHandler(); initialReview.setNext(riskAssessment); riskAssessment.setNext(finalApproval); // Simulate processing of loan applications double[] loanAmounts = {5000, 15000, 45000, 60000}; for (double amount : loanAmounts) { System.out.println("\nProcessing loan application for $" + amount); initialReview.processLoanApplication(amount); } } } /* C:\>javac LoanProcessingApplicationDemo.java C:\>java LoanProcessingApplicationDemo Processing loan application for $5000.0 Initial Review: Loan approved for $5000.0 Processing loan application for $15000.0 Risk Assessment: Loan approved for $15000.0 Processing loan application for $45000.0 Risk Assessment: Loan approved for $45000.0 Processing loan application for $60000.0 Final Approval: Loan approved for $60000.0 */
Pros
- Decoupling: Separates the sender from the receiver, as the client doesn’t need to know which handler processes the request.
- Flexibility: Easily add, remove, or reorder handlers in the chain without modifying client code.
- Single Responsibility: Each handler focuses on a specific type of request.
- Dynamic Handling: Handlers can be determined at runtime based on the request.
Cons
- No Guarantee of Handling: A request may go unhandled if no handler can process it.
- Performance Overhead: Passing a request through a long chain can introduce latency.
- Debugging Complexity: Tracing the flow of a request through multiple handlers can be challenging.
- Chain Misconfiguration: Incorrectly setting up the chain (e.g., missing links) can lead to errors.
When to Use
- When multiple objects might handle a request, and the handler should be determined dynamically.
- When you want to decouple the sender of a request from its receivers.
- When you need a flexible way to process requests in a specific order.
- When you want to support varying levels of processing or filtering (e.g., logging, authentication, validation).
Real-World Example
- Support Systems: Tickets escalate from basic to advanced support levels based on complexity.
- Event Handling in GUI: Mouse or keyboard events are passed through a chain of UI components until one handles it (e.g., Java AWT/Swing).
- Middleware in Web Frameworks: HTTP requests pass through a chain of middleware (e.g., authentication, logging, compression) in frameworks like Express.js.
- Logging Systems: Log messages are processed by different handlers (console, file, database) based on their level.
The Chain of Responsibility pattern provides a flexible and decoupled way to process requests by passing them along a chain of potential handlers. Each handler in the chain either processes the request or delegates it to the next handler. This approach promotes loose coupling between the sender and receiver, enabling scalability, easy modification, and reuse of handlers.