Chain of responsibility pattern
Martin McBride, 2022-03-08
Tags behavioural pattern chain of responsibility
Categories design patterns
Chain of responsibility is a behavioural design pattern. It is used to process command objects, where different types of command objects might need to be processed in different ways.
It does this by allowing for multiple handler objects, each capable of handling a specific type of command and rejecting other commands. The command is passed to each handler in turn, until one of the handlers is able to handle the command.
The handlers are arranged as a chain, where each handler either processes the command or passes it on to the next handler.
We often need to deal with related objects that might require different processing.
One example might be a software logging system. We would probably have various types of logging messages, such as debug, error, and critical failure. We might require that debug messages are written to an internal file, error messages are displayed to the user, but critical failures result in a message being sent back to the manufacturer.
Some of these behaviours can be complex, and new behaviours might be required in the future. So a loosely coupled system would be an advantage. This would allow us to implement each behaviour in a separate class, and to add new classes as required.
In the chain of command pattern, we implement each behaviour as a separate class which can:
- Check the command object type (for example the type of log message), and decide if it can f#handle that type.
- Process the command if it can.
- Otherwise, pass the command on to the next handler in the chain.
A client object is responsible for creating the chain of handlers. To add new functionality, all that is required is a new handler class, and a small modification to the client to add the new class into the chain.
Example - logging system
In a logging system:
- The client is the
Loggerobject that can be used to add messages to the log.
- The individual log messages are the command objects.
- The handlers each handle a specific type of log message.
Here is the class diagram:
Logger owns the chain of
MessageHandler has a
handle method, and also a link to the next handler.
Here is the runtime communication between the objects:
Here is a simple interface class,
IHandler, that provides a
handle method. That method accepts a message string.
class IHandler: def handle(self, message): pass
The first part of the string indicates what sort of message it is. This includes "info", "error", and "failure".
Here is a handler for the info case. It implements the
class InfoHandler(IHandler): def __init__(self, next): self.next = next def handle(self, message): if message.startswith("info"): pass else: self.next.handle(message)
This class is initialised with a
next object - this should be another
IHandler that will be called if the message isn't an info message.
handle method first checks the start of the message string. If it is "info" then this class will process the message, otherwise it will pass the message on to the next handler.
In this case, the handler is set up to ignore messages. Any info messages will be accepted by this handler (and therefore not passed to any other handler), but this handler will discard the message.
Here is a handler for the error case. It also implements the
class ErrorHandler(IHandler): def __init__(self, next): self.next = next def handle(self, message): if message.startswith("error"): print("ERROR", message) else: self.next.handle(message)
This time we check the message string to see if it starts with "error". If it does will handle the string, by printing it out with an ERROR label, If it doesn't match then once again we will pass it on to the next handler.
Finally here is the failure handler:
class FailureHandler(IHandler): def __init__(self, next): self.next = next def handle(self, message): if message.startswith("failure"): print("FAILURE", message) else: self.next.handle(message)
This is very similar to the error handler, except that it checks for a string that starts with "failure", and it prints a different response.
Logger is the main class for logging:
class Logger: def __init__(self): failureHandler = FailureHandler(None) errorHandler = ErrorHandler(failureHandler) infoHandler = InfoHandler(errorHandler) self.handler = infoHandler def log(self, message): self.handler.handle(message)
__init__ method creates a
ErrorHandler, and an
InfoHandler. It links them together so that:
errorHandleras its next handler.
failureHandleras its next handler.
Noneas its next handler (see discussion below).
This sets up the chain so that each handler gets a chance to process incoming log messages. Notice that the handlers are created in reverse order. This is simply because each handler requires its next handler to already exist when it is created.
Logger also has a
log method that processes log messages. It passes the message on to the first handler and lets the chain take care of it.
Running the logger
This code creates a logger, and sends some messages to it:
logger = Logger() logger.log("failure - message 1") logger.log("info - message 2") logger.log("error - message 3")
The first message is processed by
failureHandler, printing a failure message. The second is processed by
infoHandler, which discards the message. The third is processed by
errorHandler, printing an error message:
FAILURE failure - message 1 ERROR error - message 3
Extending the logger
We can easily extend the logger to add a
DebugHandler and a
WarningHandler as shown in the class diagram. We would need to define new classes, and also add them into the chain in the
As we noted earlier,
failureHandler has no next handler defined (it is set to
None). This means that if we tried to log a message that didn't match any of the handler criteria, the code would throw an exception.
We can tidy this up by using a default handler:
class DefaultHandler(IHandler): def __init__(self): pass def handle(self, message): print("Unsupported message type", message)
This class doesn't have a next handler, instead it always processes the message. This should be installed as the final handler in
We have seen how to use the chain of responsibility pattern to handle different command objects (log message) types in a loosely coupled, easily extensible way.