Chain of Responsibility Design Pattern in Java: A Practical Example


5 min read 13-11-2024
Chain of Responsibility Design Pattern in Java: A Practical Example

The Chain of Responsibility design pattern is a behavioral pattern that allows you to chain together multiple objects, each with an opportunity to handle a request. This pattern is particularly useful when you have a series of objects that can potentially process a request, and you want to avoid coupling the sender of the request with its receiver.

Understanding the Chain of Responsibility

Imagine you're at a restaurant and you need to order food. Your order might pass through several hands before it reaches the kitchen:

  • You: You place your order with a waiter.
  • Waiter: The waiter takes your order and passes it to the cashier.
  • Cashier: The cashier checks if you've paid your bill and then passes the order to the kitchen.
  • Kitchen: The kitchen prepares your food.

In this scenario, each person in the chain is responsible for a specific part of the order processing, and if they can't handle the request, they pass it on to the next person in the chain.

Similarly, in software development, the Chain of Responsibility pattern allows you to create a chain of objects, where each object can handle a specific type of request. If an object can't handle the request, it passes it on to the next object in the chain.

Benefits of the Chain of Responsibility Pattern

  • Decouples the sender and receiver: The sender of a request doesn't need to know which object will ultimately handle it.
  • Flexible and extensible: You can easily add or remove objects from the chain without affecting other parts of the system.
  • Handles multiple handlers: Allows for multiple objects to potentially handle a request.
  • Improved maintainability: The logic for handling a request is encapsulated within each object in the chain, making the code easier to understand and maintain.

Implementing the Chain of Responsibility in Java

Let's illustrate the Chain of Responsibility pattern with a practical example. Consider a scenario where we need to process a loan application. Each loan application requires a series of checks, such as credit score verification, income verification, and debt-to-income ratio calculation.

1. Define an Interface for Handlers

public interface LoanApplicationHandler {

    void process(LoanApplication application);

    LoanApplicationHandler setNext(LoanApplicationHandler handler);
}

This interface defines a process() method that each handler will implement, and a setNext() method to chain handlers together.

2. Implement Concrete Handlers

public class CreditScoreHandler implements LoanApplicationHandler {

    private LoanApplicationHandler next;

    @Override
    public void process(LoanApplication application) {
        if (application.getCreditScore() < 650) {
            System.out.println("Credit score is below the required threshold.");
            return;
        }

        if (next != null) {
            next.process(application);
        } else {
            System.out.println("Application passed credit score check.");
        }
    }

    @Override
    public LoanApplicationHandler setNext(LoanApplicationHandler handler) {
        this.next = handler;
        return handler;
    }
}

The CreditScoreHandler checks the credit score and if it falls below the threshold, it stops further processing. If it passes the check, it passes the application to the next handler in the chain.

public class IncomeHandler implements LoanApplicationHandler {

    private LoanApplicationHandler next;

    @Override
    public void process(LoanApplication application) {
        if (application.getAnnualIncome() < 50000) {
            System.out.println("Income is below the required threshold.");
            return;
        }

        if (next != null) {
            next.process(application);
        } else {
            System.out.println("Application passed income check.");
        }
    }

    @Override
    public LoanApplicationHandler setNext(LoanApplicationHandler handler) {
        this.next = handler;
        return handler;
    }
}

The IncomeHandler performs a similar check on the applicant's income.

3. Create a LoanApplication Class

public class LoanApplication {

    private int creditScore;
    private double annualIncome;
    private double debtToIncomeRatio;

    // Constructors, Getters, and Setters
}

This class represents the loan application and contains the necessary attributes.

4. Build the Chain

public class LoanApplicationProcessor {

    public static void main(String[] args) {
        LoanApplication application = new LoanApplication();
        application.setCreditScore(700);
        application.setAnnualIncome(60000);
        application.setDebtToIncomeRatio(0.3);

        LoanApplicationHandler creditScoreHandler = new CreditScoreHandler();
        LoanApplicationHandler incomeHandler = new IncomeHandler();
        LoanApplicationHandler debtToIncomeRatioHandler = new DebtToIncomeRatioHandler();

        creditScoreHandler.setNext(incomeHandler).setNext(debtToIncomeRatioHandler);

        creditScoreHandler.process(application);
    }
}

In the LoanApplicationProcessor class, we create instances of the handlers and chain them together. Finally, we process the loan application using the creditScoreHandler as the starting point.

5. Implement the Remaining Handler

The code for the DebtToIncomeRatioHandler would be similar to the previous examples, checking the applicant's debt-to-income ratio and passing the application on to the next handler if it passes the check.

Practical Use Cases

The Chain of Responsibility pattern is used in various software applications, including:

  • Event handling: In GUI applications, events are propagated through a chain of event handlers until one of them handles the event.
  • Request filtering: In web applications, a series of filters can be chained to process requests, such as authentication, authorization, or logging.
  • Workflow management: In business applications, the chain of responsibility can model the workflow of a process, where each step is handled by a specific component.
  • Command processing: The pattern can be used to implement a command-based system, where each command is processed by a chain of handlers.

Comparing the Chain of Responsibility with Other Patterns

The Chain of Responsibility pattern has similarities with other patterns, such as:

  • Observer: The Observer pattern also involves a chain of objects, but in the Observer pattern, all objects are notified of an event, whereas in the Chain of Responsibility, only one object handles the request.
  • Strategy: The Strategy pattern allows you to dynamically select an algorithm at runtime, while the Chain of Responsibility pattern allows you to dynamically select a handler at runtime.

Conclusion

The Chain of Responsibility pattern is a powerful tool for decoupling your code and creating flexible, extensible systems. By chaining together objects, you can create a flow of processing logic that handles requests in a controlled and predictable manner. While the pattern might seem complex at first, its benefits outweigh its complexity, making it a valuable addition to your design patterns arsenal.

FAQs

1. Can I have multiple handlers in a single chain that handle the same type of request?

Yes, you can have multiple handlers in a single chain that handle the same type of request. This can be useful if you need to perform different checks or actions based on different criteria. For example, you could have two credit score handlers, one that checks for a minimum score and another that checks for a specific credit rating.

2. How do I ensure that all handlers in the chain are invoked if no handler handles the request?

You can add a default handler at the end of the chain that always handles the request if no other handler in the chain can. This ensures that the request is always handled, even if it doesn't meet the criteria of any of the previous handlers.

3. What are the disadvantages of the Chain of Responsibility pattern?

The Chain of Responsibility pattern can become complex if the chain is too long or if the handlers are not well-defined. It can also be difficult to debug if a request is not handled properly.

4. Can the Chain of Responsibility pattern be used with asynchronous requests?

Yes, the Chain of Responsibility pattern can be used with asynchronous requests. This would require using asynchronous messaging techniques, such as message queues or event buses, to pass the request between handlers.

5. How can I test the Chain of Responsibility pattern?

To test the Chain of Responsibility pattern, you need to test each handler individually, as well as the interaction between handlers in the chain. You can use unit testing frameworks to test the handlers, and integration tests to test the flow of the request through the chain.