Chain of Responsibility

Definition

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

Introduction

Let us first understand what the above definition means. Next, we will look at two examples of the application of the chain of responsibility design pattern. Then, at last, I will show the pictorial class and sequence diagram for the chain of design pattern.

Chain of Responsibility

As the name suggests, there is some responsibility that each objects provides and we have got them arranged as a chain. Starting off with the client, the client initiates an action. There are multiple objects that can satisfy/provide an implementation of that action depending on the context. The objects will be linked as a linear list in order. 

The first object in the chain receiving the command or the request, looks at the context to see if it can perform that action. If yes, it does the work and the request gets satisfied by that object and the call ends there. If not, it delegates the request to the next object in the chain. It again does the same – sees if it can perform the action and if not passes the request to the next object in the chain. The next object to which each object delegates the request is called as its successor.

With this setup the object that ultimately will perform the action will not be known beforehand. Also, the caller or the client will not be aware of this chaining and delegation that is taking place. Thus, this enables us to decouple the client from the object that performs the action.

The idea of this is to decouple senders and receivers by having multiple objects an opportunity to handle a request. The request then gets passed along a chain of objects until one of them eventually handles it.

Example #1 – Logging messages

Now, we will get into our examples which will explain this pattern. Let us say we have three log levels viz., INFO, DEBUG and WARN. Then, there are three handlers – one to handle each one of the log levels. 

We define an interface for the LoggingHandler as

public interface LoggingHandler {
    void logMessage(String message, LogLevel logLevel);
}
public enum LogLevel {
    INFO, DEBUG, WARN
}

Since setting up the next handler (successor) is going to common across all handlers, we create a base class for doing this. Its default logMessage implementation will delegate to the next object in the chain (if one is present). 

public class BaseLoggingHandler implements LoggingHandler {
    private LoggingHandler successor;

    public void setNext(LoggingHandler successor) {
        this.successor = successor;
    }

    @Override
    public void logMessage(String message, LogLevel logLevel) {
        if (successor != null) {
            successor.logMessage(message, logLevel);
        }
    }
}

Now, we will look at the three implementing objects. Each will check what the request LogLevel is and decides if it can handle it or not. If it cannot handle, it calls its parent class logMessage method which will take care of delegating the request to the next object in the chain.

public class DebugLoggingHandler extends BaseLoggingHandler {
    @Override
    public void logMessage(String message, LogLevel logLevel) {
        if (logLevel == LogLevel.DEBUG) {
            System.out.println(String.format("Printing the log message '%s' as debug", message));
        } else {
            super.logMessage(message, logLevel);
        }
    }
}
public class InfoLoggingHandler extends BaseLoggingHandler {
    @Override
    public void logMessage(String message, LogLevel logLevel) {
        if (logLevel == LogLevel.INFO) {
            System.out.println(String.format("Printing the log message '%s' as info", message));
        } else {
            super.logMessage(message, logLevel);
        }
    }
}
public class WarnLoggingHandler extends BaseLoggingHandler {
    @Override
    public void logMessage(String message, LogLevel logLevel) {
        if (logLevel == LogLevel.WARN) {
            System.out.println(String.format("Printing the log message '%s' as warn", message));
        } else {
            super.logMessage(message, logLevel);
        }
    }
}

Let us look at this in action

public static void main(String[] args) {
    BaseLoggingHandler loggingHandler = new DebugLoggingHandler();
    InfoLoggingHandler infoLoggingHandler = new InfoLoggingHandler();
    WarnLoggingHandler warnLoggingHandler = new WarnLoggingHandler();

    loggingHandler.setNext(infoLoggingHandler);
    infoLoggingHandler.setNext(warnLoggingHandler);

    loggingHandler.logMessage("some message", LogLevel.DEBUG);
    loggingHandler.logMessage("some message", LogLevel.INFO);
    loggingHandler.logMessage("some message", LogLevel.WARN);

}

We create an instance of DebugLoggingHandler, InfoLoggingHandler and WarnLoggingHandler and we chain them in that order. (debug -> info -> warn). 

The first logMessage call requests to log the message as DEBUG. This will be handled by the first object in the chain(DebugLoggingHandler).

 

The second logMessage call requests to log the message as INFO. First, this request will come to the DebugLoggingHandler. It decides that it cannot handle it and the request will get delegated to the next object in the chain i.e., InfoLoggingHandler. The request will be handled by it.

Similarly, the last request will traverse through the chain and will end up at the last object in the chain (the WarnLoggingHandler).

The above code produces the below output.
Printing the log message 'some message' as debug
Printing the log message 'some message' as info
Printing the log message 'some message' as warn

Now, what if we add a new log level, ERROR, to the LogLevel enum?

public enum LogLevel {
    INFO, DEBUG, WARN, ERROR
}

And, we request a message to be logged as ERROR

loggingHandler.logMessage("some message", LogLevel.ERROR);

This just falls-through the chain without any handlers. There are a couple of ways to handle this. We will apply the first way for this example and see the second way in the next example.

We add a EndOfChainHandler handler which prints a message (unable to log) and throws an exception.

public class EndOfChainHandler extends BaseLoggingHandler {
    @Override
    public void logMessage(String message, LogLevel logLevel) {
        System.out.println("No handler found");
        throw new RuntimeException("Unable to log message of level " + logLevel);
    }
}

We chain it as

EndOfChainHandler endOfChainHandler = new EndOfChainHandler();
infoLoggingHandler.setNext(endOfChainHandler);

Now, running the below produces an exception – java.lang.RuntimeException: Unable to log message of level ERROR

loggingHandler.logMessage("some message", LogLevel.ERROR);

Example #2 – Financial approver

Let us say, in a company, there are various levels of financial approvers. We classify these approvers as low, mid and high level. The approvers in each level is capable of approving money in a certain range. 

The low-level approver can only approve if the amount to be approved is less than or equal to 1000. For amounts more than that, the low level approver cannot approve and it has to go up the financial approver chain. The mid-level approver can approve amount between 1001 – 10,000.  And finally, the high-level approver approves between 10,001-100,000.

The FinancialApprover interface is the base interface for the chain of approvers. It has an approveMoney method the client calls on to get money approved. The BaseFinancialApprover base class is there to set the successor and to call the successor if an object cannot handle the incoming request.

public interface FinancialApprover {
    void approveMoney(long amountToApprove);
}
public class BaseFinancialApprover implements FinancialApprover {
    private FinancialApprover successor;

    public void setNext(FinancialApprover successor) {
        this.successor = successor;
    }

    @Override
    public void approveMoney(long amountToApprove) {
        if (successor != null) {
            successor.approveMoney(amountToApprove);
        }
    }
}

The three implementing classes are shown below

public class LowLevelApprover extends BaseFinancialApprover {
    @Override
    public void approveMoney(long amountToApprove) {
        if (amountToApprove <= 1000) {
            System.out.println("The low level approver approving the amount");
        }
        super.approveMoney(amountToApprove);
    }
}
public class MidLevelApprover extends BaseFinancialApprover {
    @Override
    public void approveMoney(long amountToApprove) {
        if (amountToApprove > 1000 && amountToApprove <= 10000) {
            System.out.println("The mid level approver approving the amount");
        }
        super.approveMoney(amountToApprove);
    }
}
public class HighLevelApprover extends BaseFinancialApprover {
    @Override
    public void approveMoney(long amountToApprove) {
        if (amountToApprove > 10000 && amountToApprove <= 100_000) {
            System.out.println("The high level approver approving the amount");
        }
        super.approveMoney(amountToApprove);
    }
}

Let us chain the objects in order of low, mid and high.

public static void main(String[] args) {
    BaseFinancialApprover financialApprover = new LowLevelApprover();
    MidLevelApprover midLevelApprover = new MidLevelApprover();
    HighLevelApprover highLevelApprover = new HighLevelApprover();
    financialApprover.setNext(midLevelApprover);
    midLevelApprover.setNext(highLevelApprover);

    financialApprover.approveMoney(100);
    financialApprover.approveMoney(8000);
    financialApprover.approveMoney(11000);
}

The above code outputs the following:

The low level approver approving the amount
The mid level approver approving the amount
The high level approver approving the amount

What if we ask to approve an amount of more than 100,000? It falls through the chain. In the earlier example, we added a handler that throws an exception. Now, we will add an handler that can approve any amount – the TopLevelApprover.

public class TopLevelApprover extends BaseFinancialApprover {
    @Override
    public void approveMoney(long amountToApprove) {
        if (amountToApprove > 100000) {
            System.out.println("The top level approver approving the amount");
        }
        super.approveMoney(amountToApprove);
    }
}
TopLevelApprover topLevelApprover = new TopLevelApprover();
highLevelApprover.setNext(topLevelApprover);
financialApprover.approveMoney(110000);

prints,

The top level approver approving the amount

Structure

Chain of Responsibility Class Diagram
Chain of Responsibility Class Diagram
Chain of Responsibility Sequence Diagram
Chain of Responsibility Sequence Diagram

Participants

Handler (LoggingHandler, FinancialApprover)
  • Defines an interface for handling requests and optionally  implements the successor link.
 
ConcreteHandler (InfoLoggingHandler, MidLevelApprover etc)
  • Handles requests it is responsible for and access its successor.
  • If the ConcreteHandler can handle the request, it does so; otherwise it forwards the request to its successor.
 
Client
  • Initiates the request to a ConcreteHandler object on the chain.

Chain of responsibility – Pros and Cons

Pros

We use the chain of responsibility when we have more than one object that can handle a request and the specific handler isn’t known before i.e., the handler is picked dynamically based on the input/request. It enables to decouple the client issuing the request from the objects handling the request. We can even change the links dynamically and can even construct the entire chain dynamically say based on a configuration.

Cons

Setting up the chain can be tedious and maybe error prone.

Other thoughts

Note that I’ve placed the method to set the successor (setNext) in the base class. Some implementation examples will show that method on the handler interface. I do not like having that in the interface because the client will use the handler interface to send requests. Exposing the setNext on that is a bad abstraction and exposes internal implementation details. It is also dangerous – what if the client sets next to a null?
So, with the implementation shown above, we can pass the handler interface (LoggingHandler or the FinancialApprover) to the client. All the client can do it to invoke the methods on it (logMessage or approveMoney).
We can think about the chain of responsibility design pattern from two angles. 

  1. Implementing a bunch of if else conditions (if..else if..else if..else). Each of the action done in the if/else if/else blocks results in an object in the chain. (Similar to our examples)
  2. The chain of objects arranged to handle a request is arranged from specific to generic. For example, the handlers to display a help text message on an UI can be implemented using the chain of responsibility. We will place the most specific help text component first. The other generic components will be chained. Example, help text message handlers for OK button -> button -> print dialog -> generic dialog. If the OK button cannot provide a help text, then a generic button level help text will be shown and so on.

Related design patterns

The successors follow the decorator style – Decorator Design Pattern

Conclusion

We saw about the Chain of Responsibility Design Pattern. We looked at the application of it using two examples – the logging level handler and a financial approver. Then, we looked at the pros and cons of it.

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
  2. https://refactoring.guru/design-patterns/chain-of-responsibility
  3. https://sourcemaking.com/design_patterns/chain_of_responsibility
  4. https://www.journaldev.com/1617/chain-of-responsibility-design-pattern-in-java

Leave a Reply