Design Patterns: Chain of Responsibility

Alexander Schellenberg
Stackademic
Published in
9 min readJan 4, 2024

--

Photo by Joey Kyber: https://www.pexels.com/photo/selective-focus-photoraphy-of-chains-during-golden-hour-119562/

The Chain of Responsibility design pattern is a behavioural pattern that addresses the need to pass a request along a chain of handlers. Each handler in the chain has the ability to either process the request or pass it to the next handler in the chain. This pattern decouples the sender of a request from its receivers, allowing multiple objects to handle the request without the sender needing to be aware of the specifics. The chain is dynamically configured during runtime, providing a flexible and extensible way to handle varying types of requests in a sequential manner.

Intent

The intent of the Chain of Responsibility design pattern is to create a chain of handler objects where each handler is responsible for processing a specific type of request. The pattern aims to pass a request along this chain, allowing each handler the opportunity to handle the request or delegate it to the next handler in the chain. By doing so, it decouples the sender of the request from its receivers, enabling multiple objects to handle the request independently.

The key goals and intent of the Chain of Responsibility pattern include:

Avoiding Coupling

The pattern promotes loose coupling between the sender and the receivers of a request, as the sender doesn’t need to know the specific handler that will process the request.

Dynamic Request Handling

The chain can be modified or extended during runtime, allowing for dynamic configuration and adjustment of the chain of handlers.

Sequential Processing

Requests are processed sequentially through the chain of handlers, ensuring that each handler has the chance to handle or pass the request based on its specific criteria.

Flexibility and Extensibility

The pattern provides a flexible and extensible way to add, remove, or reorder handlers in the chain without modifying the client code.

Overall, the Chain of Responsibility pattern helps to achieve a more flexible and maintainable design by distributing responsibilities across a chain of collaborating objects.

Structure

The Chain of Responsibility design pattern is structured with the following key components:

Handler

  • Declares an interface or an abstract class for handling requests.
  • Contains a reference to the next handler in the chain.
// 1. Handler
public interface Handler {
void handleRequest(Request request);
void setNextHandler(Handler nextHandler);
}

ConcreteHandler

  • Implements the Handler interface or abstract class.
  • Handles requests for which it is responsible.
  • Optionally, passes the request to the next handler in the chain.
// 2. ConcreteHandler
public class ConcreteHandler1 implements Handler {
private Handler nextHandler;

@Override
public void handleRequest(Request request) {
if (request.getType().equals("Type1")) {
// Handle the request
System.out.println("ConcreteHandler1 is handling the request");
} else {
// Pass the request to the next handler
if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}

@Override
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
}

// Another ConcreteHandler
public class ConcreteHandler2 implements Handler {
private Handler nextHandler;

@Override
public void handleRequest(Request request) {
if (request.getType().equals("Type2")) {
// Handle the request
System.out.println("ConcreteHandler2 is handling the request");
} else {
// Pass the request to the next handler
if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}

@Override
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
}

Client

  • Initiates requests and forwards them to the first handler in the chain.
// 3. Client
public class Client {
public static void main(String[] args) {
// Create handlers
Handler handler1 = new ConcreteHandler1();
Handler handler2 = new ConcreteHandler2();

// Set up the chain
handler1.setNextHandler(handler2);

// Create and send requests
Request request1 = new Request("Type1");
Request request2 = new Request("Type2");

// Client initiates the request
handler1.handleRequest(request1);
handler1.handleRequest(request2);
}
}

Request

  • Represents the request to be processed by the chain of handlers.
  • Handlers decide whether to handle the request or pass it to the next handler based on specific criteria.
// 4. Request class
public class Request {
private String type;

public Request(String type) {
this.type = type;
}

public String getType() {
return type;
}
}

The structure of the Chain of Responsibility pattern allows for a dynamic and sequential processing of requests through the chain of handlers. Each handler in the chain has the ability to either handle the request or delegate it to the next handler. This sequential processing continues until the request is handled or until the end of the chain is reached.

Here is a simplified representation of the structure:

+--------------+      +---------------+      +---------------+
| Handler |<-----|ConcreteHandler|<-----|ConcreteHandler|
+--------------+ +---------------+ +---------------+
^ |
| |
+-------------------+
|
v
+--------------+
| Client |
+--------------+

In this structure, the client initiates the request and sends it to the first handler in the chain. Each concrete handler decides whether to handle the request or pass it to the next handler in the chain. The client remains unaware of the specific handler that will ultimately process the request, promoting loose coupling and flexibility.

Simplified “real world example”

Let’s consider a real-world scenario where the Chain of Responsibility pattern could be applied: an expense reimbursement system in a company.

Handler Interface

// Handler interface
public interface Approver {
void processRequest(Expense expense);
void setNextApprover(Approver nextApprover);
}

Concrete Handlers

// ConcreteHandler 1
public class TeamLead implements Approver {
private Approver nextApprover;

@Override
public void processRequest(Expense expense) {
if (expense.getAmount() <= 1000) {
System.out.println("Team Lead approves the expense of $" + expense.getAmount());
} else if (nextApprover != null) {
nextApprover.processRequest(expense);
}
}

@Override
public void setNextApprover(Approver nextApprover) {
this.nextApprover = nextApprover;
}
}

// ConcreteHandler 2
public class Manager implements Approver {
private Approver nextApprover;

@Override
public void processRequest(Expense expense) {
if (expense.getAmount() > 1000 && expense.getAmount() <= 5000) {
System.out.println("Manager approves the expense of $" + expense.getAmount());
} else if (nextApprover != null) {
nextApprover.processRequest(expense);
}
}

@Override
public void setNextApprover(Approver nextApprover) {
this.nextApprover = nextApprover;
}
}

// ConcreteHandler 3
public class Director implements Approver {
@Override
public void processRequest(Expense expense) {
if (expense.getAmount() > 5000) {
System.out.println("Director approves the expense of $" + expense.getAmount());
} else {
System.out.println("Expense not approved.");
}
}

@Override
public void setNextApprover(Approver nextApprover) {
// Director is the top-level approver and doesn't have a next approver.
}
}

Client

public class ExpenseClient {
public static void main(String[] args) {
// Create handlers
Approver teamLead = new TeamLead();
Approver manager = new Manager();
Approver director = new Director();

// Set up the chain
teamLead.setNextApprover(manager);
manager.setNextApprover(director);

// Create and process expenses
Expense expense1 = new Expense(800);
Expense expense2 = new Expense(3000);
Expense expense3 = new Expense(7000);

// Client initiates the request
teamLead.processRequest(expense1);
teamLead.processRequest(expense2);
teamLead.processRequest(expense3);
}
}

Expense Class

public class Expense {
private double amount;

public Expense(double amount) {
this.amount = amount;
}

public double getAmount() {
return amount;
}
}

In this example, the Approver interface represents the handler, and TeamLead, Manager, and Director are concrete handlers. Each handler in the chain has the responsibility to approve expenses based on certain criteria. The client initiates an expense request, and the request is processed through the chain of handlers. If a handler can't approve the expense, it delegates the request to the next handler in the chain. This way, the responsibility for approving expenses is distributed among different levels of authority in the company.

Integration with Other Design Patterns

The Chain of Responsibility design pattern can be used in conjunction with other design patterns to create more flexible and modular solutions. Here are a few examples of how the Chain of Responsibility pattern can integrate with other design patterns:

Decorator Pattern

  • The Chain of Responsibility can be combined with the Decorator pattern to add responsibilities dynamically to objects in the chain.
  • Each handler in the chain can be a decorator, enhancing the behavior of the object it decorates.
  • This combination allows for a flexible and dynamic composition of responsibilities.

Command Pattern

  • The Command pattern can be used to encapsulate requests as objects, and these command objects can be passed along the chain.
  • Each handler in the chain becomes a command processor, responsible for executing the command it receives.
  • This integration allows for a separation of sender and receiver, providing a more flexible way to handle requests.

Observer Pattern

  • The Chain of Responsibility can be combined with the Observer pattern to notify multiple observers (handlers) about a particular event or request.
  • Observers can be dynamically added or removed from the chain, providing flexibility in handling events.

Strategy Pattern

  • The Strategy pattern can be employed to encapsulate different algorithms or strategies for handling requests.
  • Handlers in the chain can dynamically switch between different strategies based on the nature of the request.
  • This integration enhances the flexibility of the Chain of Responsibility pattern by allowing interchangeable algorithms.

Composite Pattern

  • The Composite pattern can be combined with the Chain of Responsibility to create hierarchical structures of handlers.
  • Each handler can act as a leaf or a composite, allowing the creation of complex structures for handling requests.
  • This combination supports both individual and group handling of requests.

State Pattern

  • The State pattern can be integrated to represent the internal state of handlers in the chain.
  • Handlers can change their behaviour dynamically based on their state, providing a way to manage different processing modes.

It’s important to note that the effectiveness of combining design patterns depends on the specific requirements of the problem at hand. The integration of patterns should be done thoughtfully to address specific concerns and promote a maintainable and scalable design.

Best Practices and Tips

When using the Chain of Responsibility design pattern, there are several best practices and tips that can help you create a well-designed and effective implementation:

Clearly Define Responsibilities

Ensure that each handler in the chain has a clearly defined and specific responsibility. This promotes a clean and understandable design.

Avoid Overly Complex Chains

Keep the chain simple and avoid overly complex structures. A clear and straightforward chain is easier to understand, maintain, and debug.

Ensure Responsiveness

Handlers should process requests promptly. Avoid unnecessary delays in processing by each handler.

Set Up Default Handling

Consider providing a default or fallback handler at the end of the chain to handle requests that none of the handlers can process. This prevents requests from going unhandled.

Dynamic Configuration

Make the chain dynamically configurable. Allow for easy addition or removal of handlers during runtime to adjust the behaviour of the application.

Handle Unwanted Requests Gracefully

Handlers should gracefully pass on requests that they cannot handle rather than rejecting or causing errors. This ensures a smoother flow through the chain.

Promote Loose Coupling

Aim for low coupling between the client and the handlers. The client should not have detailed knowledge of the handlers in the chain.

Consider Performance Implications

Be mindful of the potential performance impact of a long chain. Large chains may introduce overhead, so it’s essential to balance modularity with efficiency.

Logging and Debugging

Implement logging within handlers to facilitate debugging. Logging can help trace the flow of requests through the chain and identify any issues.

Use Cases

Understand the nature of the use cases where the Chain of Responsibility is beneficial. It is particularly useful when there are multiple objects that can handle a request, and the client should not be concerned with the specific handler.

Single Responsibility Principle

Ensure that each handler adheres to the Single Responsibility Principle. A handler should have only one reason to change, and that is when the logic for handling a specific request changes.

Documentation

Provide clear documentation for each handler’s responsibility, the order in which they are arranged, and any assumptions about the requests they can handle.

By following these best practices, you can create a Chain of Responsibility implementation that is modular, maintainable, and well-suited for the specific requirements of your application.

Wrapping Up

The Chain of Responsibility design pattern is a powerful behavioural pattern that promotes a flexible and decoupled way of handling requests in a sequential manner. It allows a chain of handlers to process a request, with each handler having the option to handle the request or pass it to the next handler in the chain. This pattern is particularly beneficial in scenarios where multiple objects may handle a request, and the client should remain unaware of the specific handler.

By applying the Chain of Responsibility pattern judiciously and following best practices, developers can create scalable, modular, and extensible systems that efficiently handle varying types of requests in a flexible and maintainable manner.

Happy coding!

Disclaimer

Kindly note that this content contains Amazon affiliate links. Your use of these links is a valuable way to support me without incurring any additional costs on your part. The content was initially generated with the assistance of AI, providing a general outline. If you have any questions or concerns, please don’t hesitate to reach out. Your support is greatly appreciated!

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--

Devoted software artisan passionate about refactoring: sculpting code into masterpieces for enhanced efficiency and clarity.