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.

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 has errorHandler as its next handler.
  • errorHandler has failureHandler as its next handler.
  • failureHandler has None 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.

If you found this article useful, you might be interested in the book Functional Programming in Python or other books by the same author.

Prev

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 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 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 polygon positional parameter print 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 text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip