The Chain of Responsibility is a behavioral design pattern that helps in passing a request along a chain of handlers.
The Chain of Responsibility pattern is like a relay race. In a relay race, each runner passes the baton to the next runner. Similarly, in this pattern, a request is passed along a chain of handlers.
Each handler decides either to process the request or to pass it along the chain. This pattern promotes loose coupling and allows multiple objects to handle a request without the sender needing to know which object will ultimately process it.
Components
Here are the main components of the Chain of Responsibility pattern:
This defines the interface for handling the requests. It usually contains a method for handling the request and a reference to the next handler in the chain.
Concrete Handlers:
These are the actual handlers that implement the Handler interface. Each concrete handler has a reference to the next handler in the chain.
Client:
This is the object that initiates the request. It sends the request to the first handler in the chain.
Benefits
Loose Coupling: The sender doesn’t need to know which object will ultimately process the request. It only needs to know the first handler in the chain.
Dynamic Handling: Handlers can be added, removed, or reordered dynamically without affecting the client’s code.
Single Responsibility: Each handler has the responsibility to process a specific type of request.
Drawbacks
Unprocessed Requests: If the chain is not configured properly, a request might go unprocessed.
Performance Overhead: If the chain is long, it can add some performance overhead as each handler is invoked in sequence.
Applications
Middleware in Web Applications:
In web development, middleware functions often use the Chain of Responsibility pattern. Each middleware can process an HTTP request and either handle it or pass it to the next middleware.
Event Handling:
In GUI programming, events are often handled using a chain of event handlers. Each handler checks if it can handle the event, and if not, passes it to the next handler.
Logging and Error Handling:
Loggers can use the Chain of Responsibility to determine where to output log messages based on their severity.
Remember, the key idea is that the responsibility for handling a request is spread across a chain of handlers, with each handler either processing the request or passing it to the next handler in line.
Examples
Expense approval in a company
Let’s implement the Chain of Responsibility design pattern using a real-world example of expense approval in a company. In this scenario, expenses need to go through a chain of approvers, each with a different spending limit authority.
publicclassTeamLeadimplementsApprover{privatedoubleapprovalLimit=1000;privateApprovernextApprover;@OverridepublicvoidsetNextApprover(ApprovernextApprover){this.nextApprover=nextApprover;}@OverridepublicvoidapproveExpense(Expenseexpense){if(expense.getAmount()<=approvalLimit){System.out.println("Team Lead approved the expense of $"+expense.getAmount());}elseif(nextApprover!=null){nextApprover.approveExpense(expense);}}}publicclassManagerimplementsApprover{privatedoubleapprovalLimit=5000;privateApprovernextApprover;@OverridepublicvoidsetNextApprover(ApprovernextApprover){this.nextApprover=nextApprover;}@OverridepublicvoidapproveExpense(Expenseexpense){if(expense.getAmount()<=approvalLimit){System.out.println("Manager approved the expense of $"+expense.getAmount());}elseif(nextApprover!=null){nextApprover.approveExpense(expense);}}}publicclassFinanceManagerimplementsApprover{privatedoubleapprovalLimit=10000;@OverridepublicvoidsetNextApprover(ApprovernextApprover){// Finance Manager is the highest authority and doesn't have a next approver}@OverridepublicvoidapproveExpense(Expenseexpense){if(expense.getAmount()<=approvalLimit){System.out.println("Finance Manager approved the expense of $"+expense.getAmount());}else{System.out.println("Expense exceeds the maximum limit. Additional approvals required.");}}}
publicclassExpenseApprovalClient{publicstaticvoidmain(String[]args){// Create ApproversApproverteamLead=newTeamLead();Approvermanager=newManager();ApproverfinanceManager=newFinanceManager();// Chain them togetherteamLead.setNextApprover(manager);manager.setNextApprover(financeManager);// Create expensesExpenseexpense1=newExpense(500);// Team Lead approvesExpenseexpense2=newExpense(4500);// Manager approvesExpenseexpense3=newExpense(12000);// Finance Manager approves// Submit expenses for approvalteamLead.approveExpense(expense1);System.out.println("----------------------");teamLead.approveExpense(expense2);System.out.println("----------------------");teamLead.approveExpense(expense3);}}
Step 5: Run the Client Class
When you run the ExpenseApprovalClient, you will see the output indicating which approver approved each expense.
Output:
Team Lead approved the expense of $500
----------------------
Manager approved the expense of $4500
----------------------
Finance Manager approved the expense of $12000
Customer Support Ticketing System
Let’s consider a real-world example of a customer support ticketing system. In this scenario, a support ticket goes through different levels of support agents until it is resolved.
publicclassLevel1SupportimplementsSupportHandler{privateSupportHandlernextHandler;@OverridepublicvoidsetNextHandler(SupportHandlernextHandler){this.nextHandler=nextHandler;}@OverridepublicvoidhandleTicket(Ticketticket){if(ticket.getPriority()<=2){System.out.println("Level 1 Support handled ticket #"+ticket.getId());}elseif(nextHandler!=null){nextHandler.handleTicket(ticket);}}}publicclassLevel2SupportimplementsSupportHandler{privateSupportHandlernextHandler;@OverridepublicvoidsetNextHandler(SupportHandlernextHandler){this.nextHandler=nextHandler;}@OverridepublicvoidhandleTicket(Ticketticket){if(ticket.getPriority()<=4){System.out.println("Level 2 Support handled ticket #"+ticket.getId());}elseif(nextHandler!=null){nextHandler.handleTicket(ticket);}}}publicclassLevel3SupportimplementsSupportHandler{@OverridepublicvoidsetNextHandler(SupportHandlernextHandler){// Level 3 Support is the highest authority and doesn't have a next handler}@OverridepublicvoidhandleTicket(Ticketticket){System.out.println("Level 3 Support handled ticket #"+ticket.getId());}}
publicclassTicketingSystemClient{publicstaticvoidmain(String[]args){// Create Support HandlersSupportHandlerlevel1=newLevel1Support();SupportHandlerlevel2=newLevel2Support();SupportHandlerlevel3=newLevel3Support();// Chain them togetherlevel1.setNextHandler(level2);level2.setNextHandler(level3);// Create tickets with different prioritiesTicketticket1=newTicket(101,3);// Level 2 Support handlesTicketticket2=newTicket(102,1);// Level 1 Support handlesTicketticket3=newTicket(103,5);// Level 3 Support handles// Submit tickets for handlinglevel1.handleTicket(ticket1);System.out.println("----------------------");level1.handleTicket(ticket2);System.out.println("----------------------");level1.handleTicket(ticket3);}}
Step 5: Run the Client Class
When you run the TicketingSystemClient, you will see the output indicating which support handler handled each ticket.
Output:
Level 2 Support handled ticket #101
----------------------
Level 1 Support handled ticket #102
----------------------
Level 3 Support handled ticket #103
Functional Programming
In a functional programming environment, we can implement the Chain of Responsibility pattern using higher-order functions and recursion. Instead of using classes and objects, we’ll rely on functions to process requests.
Let’s adapt the customer support ticketing example to a functional programming style using Java 8+ features:
Step 1: Define Support Handlers as Functions
importjava.util.function.BiConsumer;importjava.util.function.Consumer;publicclassFunctionalTicketingSystem{publicstaticConsumer<Ticket>level1Support=ticket->{if(ticket.getPriority()<=2){System.out.println("Level 1 Support handled ticket #"+ticket.getId());}else{level2Support.accept(ticket);}};publicstaticConsumer<Ticket>level2Support=ticket->{if(ticket.getPriority()<=4){System.out.println("Level 2 Support handled ticket #"+ticket.getId());}else{level3Support.accept(ticket);}};publicstaticConsumer<Ticket>level3Support=ticket->System.out.println("Level 3 Support handled ticket #"+ticket.getId());}
importjava.util.function.Consumer;publicclassTicketingSystemClient{publicstaticvoidmain(String[]args){Consumer<Ticket>ticketHandler=FunctionalTicketingSystem.level1Support;// Create tickets with different prioritiesTicketticket1=newTicket(101,3);// Level 2 Support handlesTicketticket2=newTicket(102,1);// Level 1 Support handlesTicketticket3=newTicket(103,5);// Level 3 Support handles// Submit tickets for handlingticketHandler.accept(ticket1);System.out.println("----------------------");ticketHandler.accept(ticket2);System.out.println("----------------------");ticketHandler.accept(ticket3);}}
Step 4: Run the Client Class
When you run the TicketingSystemClient, you will see the output indicating which support handler handled each ticket.
Output:
Level 2 Support handled ticket #101
----------------------
Level 1 Support handled ticket #102
----------------------
Level 3 Support handled ticket #103