Chain of responsibility pattern
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.
Motivation
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
Logger
object 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:
The Logger
owns the chain of MessageHandlers
. Each MessageHandler
has a handle
method, and also a link to the next handler.
Here is the runtime communication between the objects:
Handler classes
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 IHandler
interface:
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.
The 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 IHandler
interface:
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 class
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)
The __init__
method creates a FailureHandler
, an ErrorHandler
, and an InfoHandler
. It links them together so that:
infoHandler
haserrorHandler
as its next handler.errorHandler
hasfailureHandler
as its next handler.failureHandler
hasNone
as 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.
The 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 Logger.__init__()
function.
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 Logger.__init__()
.
Summary
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.
See also
Join the PythonInformer Newsletter
Sign up using this form to receive an email when new content is added:
Popular tags
2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest