Effortless Elegance: Understanding Chain of Responsibility in Python

Introduction

The Chain of Responsibility pattern describes a chain of command/request receivers. The client has no idea which one of the receivers will handle the request. The beauty of the pattern is again uncoupling: the sender and the receiver do not know about each other. A danger is that no receiver in the chain will handle the request, this has to be taken into account when implementing this pattern.

An excellent example of this could be the way that middleware is implemented in some web-frameworks (Gin, but also ASP.NET core are examples).

It looks like this:

Let us break this down:

  1. We have a Client who does a request to a Handler
  2. The Handler has a list of Receivers, and succesively sends the request to a receiver until one handles the request

As you can see, this is not a very complicated pattern

Implementation in Python

Prerequisites

Since we will implement a class that will be referring to itself, we need the following opening line:

from __future__ import annotations
from abc import ABC, abstractmethod

Line by line:

  1. In the Handler we need to be able to refer to the class itself, so we need to import annotations. To be precise, this import enables PEP 563, postponed evaluation of annotation. This allows forward references without quotes.
  2. The Handler class will be an abstract base class, that is why the ABC is needed. Also, the handle() method will be implemented differently in each subclass, so we need the @abstractmethod decorator.

The Handler base class

We will start by defining the Handler base class:

class Handler(ABC):

    def __init__(self, next_handler: Handler | None  = None):
        self._next_handler = next_handler

    def set_next(self, handler: Handler) -> Handler:
        self._next_handler = handler
        return handler

    @abstractmethod
    def handle(self, request: str) -> str | None:
        pass

    def _handle_next(self, request: str) -> str | None:
        if self._next_handler:
            return self._next_handler.handle(request)
        return None

    def __str__(self) -> str:
        return self.__class__.__name__

Line by line:

  1. In the constructor, the next handler in the chain can be set. Note that if no handler is supplied the default value is None
  2. When you can not set the next handler, change it, or remove it, this is what the set_next() method does. This method returns the handler supplied in the parameter list, as this allows method chaining.
  3. handle() is used to perform the actual action. Because this will be handled differently by different subclasses of the Handler class, this method has been decorated with the @abstractmethod decorator. This method returns either a string or None depending on the result of handling the request.
  4. At the end of each implementation, if we want the chain to function properly we need to call the _handle_next() method which calls the next handler in the chain, and returns the result. If no handler is available it returns None.
  5. The __str__ method returns some basic class information.

Implementing the handlers

Next we define the first handler imaginatively called HandlerA:

class HandlerA(Handler):

    def handle(self, request: str) -> str | None:
        if request == "A":
            return f"{self}: processed the request."
        return self._handle_next(request)

A short breakdown:

  1. HandlerA is a subclass of Handler. The only method we need to implement is the handle() method, which handles the specific handling of the requests.
  2. In the method we check if we need to handle the request, i.e. if the request is the character A. If so, we handle it, and the chain ends.
  3. If not, we call the next handler in the chain, if there is one.

Next we define two other handlers which work similarly:

class HandlerB(Handler):
    def handle(self, request: str) -> str | None:
        if request == "B":
            return f"{self}: processed the request."
        return self._handle_next(request)


class HandlerC(Handler):
    def handle(self, request: str) -> str | None:
        if request == "C":
            return f"{self}: processed the request."
        return self._handle_next(request)

Finally we create a default handler which is at the end of the chain. This simply returns an error message:

class DefaultHandler(Handler):
    def handle(self, request: str) -> str | None:
        return f"No handler found for request: '{request}'"

Creating the chain

Finally we create a small function to create this handler chain:

def create_handler_chain() -> Handler:
    handler_a = HandlerA()
    handler_b = HandlerB()
    handler_c = HandlerC()
    default_handler = DefaultHandler()
    
    handler_a.set_next(handler_b).set_next(handler_c).set_next(default_handler)
    
    return handler_a

Some remarks:

  1. We start by creating four handlers, the last one being the DefaultHandler .
  2. Next we set handler_b as next in the chain from handler_a and so, with the last handler in the chain being the default_handler
  3. All we need to do now is return handler_a which is the head of the chain.

Time to test

Now we can see whether our setup works:

if __name__ == "__main__":

    handler_chain = create_handler_chain()
    
    test_requests = ["A", "B", "C", "D", "E"]
    
    print("Chain of Responsibility Pattern Demo")
    print("=" * 40)
    
    for request in test_requests:
        result = handler_chain.handle(request)
        print(f"Request '{request}': {result}")

Line by line:

  1. We start by creating the chain.
  2. Next we set up a list of possible request. Note that we have no handlers for a D or an E request as we shall see.
  3. We iterate over the list, and call handle() on the chain. Three of the five requests will be handles, the last will be handled by the default handler.

Conclusion

The Chain of Responsibility pattern offers a powerful way to decouple senders from receivers, promoting flexibility and maintainability in your code. As demonstrated, its implementation in Python is straightforward, relying on an abstract Handler class and concrete subclasses that decide whether to process a request or pass it along the chain. This pattern is particularly valuable in scenarios like middleware pipelines, where requests flow through a series of handlers until one takes action. While careful consideration is needed to prevent requests from being unhandled, the Chain of Responsibility effectively simplifies complex conditional logic by distributing responsibilities among a clear, sequential structure of objects.

The Code Nomad
The Code Nomad
Articles: 167

Leave a Reply

Your email address will not be published. Required fields are marked *