Chain of Responsibility Design Pattern

The chain of responsibility is similar to a linked list. Here the node is the class that has the knowledge of the next class and the value is the method handler. This is mainly used in vending machines like ATMs and logging frameworks.

Design ATM Vending machine

If we want to withdraw 2600 units of money, the ATM will first check whether it can dispense the entire amount using 2000-unit notes. If it can only partially handle the amount, such as 2000 units, the remaining 600 units will be passed on to the next handler, which is the 500-unit note handler. This handler can process only 500 units of the 600, and will pass the responsibility of the remaining 100 units to the next handler. The final handler will fulfill this amount.

So, in the end, we get one 2000-unit note, one 500-unit note, and one 100-unit note. This is how the ATM machine is designed.

Design Logging

In our logging system design, we've developed an abstract class that serves as a template for loggers. This abstract class contains the logger's fundamental structure, including a private variable that holds the reference to the next logger and a log method for passing logs to the next logger. It's worth noting that at this stage, the loggers only perform log passing, with no additional functionality. Additionally, there are three static variables for storing mapped values related to info, error, and debug logs.

All other request handlers or loggers in the system extend this base class. To create a logger, you must provide the reference to the next logger in the constructor during initialization. If there are no subsequent loggers, you can pass null as the reference.

Code

Logging Processor:

package Processor;

public abstract class LogProcessor {

    public static int INFO = 1;
    public static int DEBUG = 2;
    public static int ERROR = 3;

    private LogProcessor nextLoggerProcessor;

    public LogProcessor(LogProcessor nextLoggerProcessor){
        this.nextLoggerProcessor = nextLoggerProcessor;
    }

    public void log(int logLevel, String message) {
        if (nextLoggerProcessor != null) {
            nextLoggerProcessor.log(logLevel, message);
        }
    }
}

Request Handlers:

package Processor;

public class InfoLogProcessor extends LogProcessor{
    public InfoLogProcessor(LogProcessor nextLoggerProcessor) {
        super(nextLoggerProcessor);
    }

    @Override
    public void log(int logLevel, String message) {
        if (logLevel == INFO){
            System.out.println("INFO: "+ message);
        } else {
            super.log(logLevel, message);
        }
    }
}
package Processor;

public class ErrorLogProcessor extends LogProcessor{
    public ErrorLogProcessor(LogProcessor nextLoggerProcessor) {
        super(nextLoggerProcessor);
    }

    @Override
    public void log(int logLevel, String message) {
        if (logLevel == ERROR){
            System.out.println("ERROR: "+ message);
        } else {
            super.log(logLevel, message);
        }
    }
}
package Processor;

public class DebugLogProcessor extends LogProcessor{
    public DebugLogProcessor(LogProcessor nextLoggerProcessor) {
        super(nextLoggerProcessor);
    }

    @Override
    public void log(int logLevel, String message) {
        if (logLevel == DEBUG){
            System.out.println("DEBUG: "+ message);
        } else {
            super.log(logLevel, message);
        }
    }
}

Usage:

import Processor.DebugLogProcessor;
import Processor.ErrorLogProcessor;
import Processor.InfoLogProcessor;
import Processor.LogProcessor;

public class Main {
    public static void main(String[] args) {
        LogProcessor logProcessor = new InfoLogProcessor(new DebugLogProcessor(new ErrorLogProcessor(null)));

        logProcessor.log(LogProcessor.ERROR, "Exceptions occurred");
        logProcessor.log(LogProcessor.INFO, "Just for info");
        logProcessor.log(LogProcessor.DEBUG, "Need to debug this");
    }
}

Another Example - Order Processing

There is a request we want to process. Before processing the request, we perform a bunch of tasks:

  • Authentication

  • Authorization

  • Validation

  • Caching

Some of these tasks are not always required. So, we first call Authentication, then Authorization, followed by Validation, and finally Caching.

We pass on the responsibility from one task to the next.

Visualization

Design Payment Processing

We have different types of payment processing, such as banking server processing, credit card server processing, and PayPal server processing.

Depending on the amount, we send the processing request to a particular server.

  • If the amount is less than 500, the bank can process it.

  • If the amount is less than 1000, the credit card can process it.

  • If the amount is less than 1500, PayPal can process it.

Code

Payment Handler Abstract class

public abstract class Payment {
    Payment nextProcessor;

    public void setNextProcessor(Payment nextProcessor) {
        this.nextProcessor = nextProcessor;
    }

    public abstract void handlePayment(int amount);
}

Bank Processor

public class BankPayment extends Payment{
    @Override
    public void handlePayment(int amount) {
        if(amount<500){
            System.out.println("processed the "+amount+" in bank server");
        }
        else{
            nextProcessor.handlePayment(amount);
        }
    }
}

Credit Card Processor

public class CreditPayment extends Payment{

    @Override
    public void handlePayment(int amount) {
        if (amount<1000){
            System.out.println("processed the "+amount+" in credit server");
        }
        else {
            nextProcessor.handlePayment(amount);
        }
    }
}

Paypal Processor

public class PaypalPayment extends Payment{
    @Override
    public void handlePayment(int amount) {
        if (amount<1500){
            System.out.println("processed the "+amount+" in paypal server");
        }
        else if (nextProcessor!=null){
            nextProcessor.handlePayment(amount);
        }
        else {
            System.out.println("couldn't process the payment");
        }
    }
}

Main class

public class Main {
    public static void main(String[] args) {
        Payment bankProcessor = new BankPayment();
        Payment creditProcessor = new CreditPayment();
        Payment paypalProcessor = new PaypalPayment();

        bankProcessor.setNextProcessor(creditProcessor);
        creditProcessor.setNextProcessor(paypalProcessor);

        bankProcessor.handlePayment(1000);
        bankProcessor.handlePayment(200);
        bankProcessor.handlePayment(500);
    }
}

/*
processed the 1000 in paypal server
processed the 200 in bank server
processed the 500 in credit server
*/

Conclusion

In the Chain of Responsibility pattern, the core component is the abstract class. It should have two main elements: the next handler and the method to handle the payment. Optionally, it can also include a constructor or a setter to set the next handler.

The reason for using an abstract class instead of an interface is that we want to have the next handler field, which is common for everything. Additionally, the abstract class provides a default implementation if the handler doesn’t have any or if there is no next handler.