Behavioral patterns : Chain of Responsibility

Abstract

The Chain of Responsibility design pattern, also known as Chain of Command, is a powerful tool in the arsenal of software designers. This behavioral pattern allows requests to be passed along a chain of handlers, each of which decides whether to process it or pass it on to the next handler in the chain. In this article, we will explore the fundamental concepts of Chain of Responsibility, examine a practical example of its application in a real-world scenario, and consider its advantages and disadvantages.

Origins and Fundamental Concepts

The Chain of Responsibility pattern has its roots in the world of object-oriented programming and software engineering. Its aim is to decouple the sender of a request from its receiver by allowing more than one object to handle the request (Gamma et al., 1994). The key to this pattern lies in constructing a chain of handler objects, each of which has the ability to process the request or pass it on to the next in the chain.

In its most basic form, the chain consists of a series of nodes (or handlers) connected together. When a request arrives at the beginning of the chain, the first handler attempts to process it. If the handler can handle the request, it does so, and the chain stops. If the handler cannot handle the request, it passes it on to the next handler in the chain, and so on until the request is processed or the end of the chain is reached.

Real-World Example: Expense Approval System

Let's imagine a common scenario in many organizations: an expense approval system. In this system, employees submit requests for expenses that must be approved by different levels of authority, depending on the amount of the expense. For example, a minor expense may require approval from a supervisor, while a major expense may require approval from a department manager or even a CEO.

In this context, we can apply the Chain of Responsibility pattern as follows:

  • We define a base class for all request handlers.

  • Each concrete handler class represents a different level of authority for approving expenses.

  • When an expense request arrives at the system, it is passed to the first handler in the chain.

  • Each handler decides whether it can approve the expense based on its level of authority and the amount of the expense.

  • If a handler can approve the expense, it does so, and the chain stops.

  • If a handler cannot approve the expense, it passes it on to the next handler in the chain.

Pattern Implementation

// Definition of the interface for request handlers
public interface Handler {
    void handleRequest(Request request);
}

// Base class for all request handlers
public abstract class BaseHandler implements Handler {
    private Handler next;

    public void setNext(Handler next) {
        this.next = next;
    }

    public void passRequestToNext(Handler next, Request request) {
        if (next != null) {
            next.handleRequest(request);
        }
    }
}

// Concrete handler class for supervisor expense approval
public class Supervisor extends BaseHandler {
    @Override
    public void handleRequest(Request request) {
        if (request.getAmount() <= 100) {
            System.out.println("Supervisor has approved the expense of $" + request.getAmount());
        } else {
            passRequestToNext(this.next, request);
        }
    }
}

// Concrete handler class for department manager expense approval
public class DepartmentManager extends BaseHandler {
    @Override
    public void handleRequest(Request request) {
        if (request.getAmount() <= 500) {
            System.out.println("Department manager has approved the expense of $" + request.getAmount());
        } else {
            passRequestToNext(this.next, request);
        }
    }
}

// Concrete handler class for CEO expense approval
public class CEO extends BaseHandler {
    @Override
    public void handleRequest(Request request) {
        System.out.println("CEO has approved the expense of $" + request.getAmount());
    }
}

// Class representing an expense request
public class Request {
    private double amount;

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

    public double getAmount() {
        return amount;
    }
}

// Example of using the Chain of Responsibility pattern
public class Main {
    public static void main(String[] args) {
        // Configuration of the handler chain
        Handler supervisor = new Supervisor();
        Handler departmentManager = new DepartmentManager();
        Handler ceo = new CEO();
        supervisor.setNext(departmentManager);
        departmentManager.setNext(ceo);

        // Example expense requests
        Request request1 = new Request(50);
        Request request2 = new Request(200);
        Request request3 = new Request(1000);

        // Processing the requests
        supervisor.handleRequest(request1);
        supervisor.handleRequest(request2);
        supervisor.handleRequest(request3);
    }
}

Advantages and Disadvantages

The Chain of Responsibility pattern offers several advantages, such as flexibility and ease of adding new handlers or modifying the behavior of existing ones (Gamma et al., 1994). However, it can also introduce additional complexity and make it more difficult to track the flow of a request through the chain.

AdvantagesDisadvantages
Flexibility in handling requestsIncreased system complexity
Ease of adding new handlers or modifying the behavior of existing onesDifficulty in tracking the flow of a request through the chain
Decoupling between the sender and receiver of requestsPossibility of a request not being handled if the chain is not properly configured

Applying Chain of Responsibility in Modern Software Development

In today's rapidly evolving landscape of software development, the Chain of Responsibility pattern continues to play a significant role in addressing various design challenges. Modern software architectures often require flexible and scalable solutions to handle complex workflows and business logic. The Chain of Responsibility pattern offers a way to manage these challenges by providing a decoupled and extensible mechanism for processing requests.

One aspect of applying the Chain of Responsibility pattern in modern software development is its integration with other design patterns and architectural styles. For example, the pattern can be combined with the Observer pattern to notify multiple observers about the outcome of a request processing, or with the Command pattern to encapsulate requests as objects and pass them along the chain.

Another important consideration is the implementation of the pattern in specific development environments and frameworks. For instance, in the context of web development, the Chain of Responsibility pattern can be utilized to handle HTTP requests in a middleware pipeline, where each middleware component in the chain processes the request and passes it along to the next component.

Moreover, the advent of microservices architecture has further highlighted the relevance of the Chain of Responsibility pattern in distributed systems. Each microservice can act as a handler in the chain, responsible for processing specific types of requests, while allowing for independent deployment and scaling of individual components.

In conclusion, the Chain of Responsibility pattern remains a valuable tool in the toolkit of modern software developers, offering a flexible and adaptable approach to managing request processing in a wide range of application scenarios.

Conclusion

In summary, the Chain of Responsibility pattern is a valuable tool for managing requests in a flexible and decoupled manner in software systems (Freeman et al., 2004). By modeling responsibilities as a chain of handlers, we can simplify the design and facilitate the extension and modification of the system's behavior.

References