TUTORIAL SERIES

Design Patterns in Python: Chain of Responsibility 🔗

Seamless Request Handling

Amir Lavasani
10 min readOct 23, 2023

Have you encountered recurring coding challenges? Imagine having a toolbox of tried-and-true solutions readily available. That’s precisely what design patterns provide. In this series, we’ll explore what these patterns are and how they can elevate your coding skills.

Understanding the Chain of Responsibility Pattern

Imagine you’re tasked with designing a system where multiple processing stages must handle incoming requests. Each stage has a specific responsibility, and the order in which these stages are executed is crucial. This is precisely where the Chain of Responsibility Design Pattern comes into play.

What is the Chain of Responsibility Design Pattern?

The Chain of Responsibility Design Pattern is a behavioral pattern that provides a solution for passing requests along a chain of handlers.

These handlers, like links in a chain, process the request or pass it to the next handler in line. This pattern acts as an intermediary, allowing you to decouple the sender of a request from its receivers.

When to Use the Chain of Responsibility Pattern

The Chain of Responsibility pattern is applicable in various scenarios where you need to streamline request processing:

  1. Request Processing: In situations where requests must pass through multiple processing stages, each handling a specific task. If one stage fails, the request is forwarded to the next, ensuring efficient processing.
  2. Logging: Logging systems with multiple log handlers like console, file, and email loggers. Handlers decide whether to handle a log message based on severity. If a handler can’t, it gracefully passes it to the next in the chain.
  3. User Interface Events: For user interface components, such as buttons, that handle events through a chain of listeners. Each listener can decide whether to consume or propagate the event, offering flexibility in complex interfaces.
A series of falling dominoes, with each domino representing a handler in the chain
Dall-E generated image with the following concept: A series of falling dominoes, with each domino representing a handler in the chain

Chain of Responsibility Anatomy

The Chain of Responsibility pattern is composed of key components and structured classes that work harmoniously to efficiently manage the flow of requests.

Key Components

  1. Handler (Abstract Class or Interface): The Handler is an abstract class or interface defining the common interface for concrete handlers. It typically includes the handle_request method, specifying how requests are processed. Concrete handlers must extend or implement this.
  2. Concrete Handlers: Concrete Handlers are classes extending the Handler. They represent processing stages in the chain, responsible for handling specific requests. Each concrete handler processes a request or passes it to the next.

Structure Classes

  1. Client: The Client initiates requests and sends them to the first handler in the chain. It remains unaware of specific handlers and their responsibilities. The client creates and configures the chain.
  2. Chain: The Chain class manages the sequence of handlers, maintaining them in an ordered list or other data structures. Its role is to pass requests from one handler to the next until successful processing or reaching the end of the chain.
Chain of Responsibility Design Pattern Class Diagram
Chain of Responsibility Design Pattern Class Diagram. Image from refactoring.guru

Types of Chain of Responsibility

The Chain of Responsibility pattern can manifest in various forms, each catering to specific use cases:

  1. Basic Chain: In this standard form, handlers are linked sequentially, and each handler either processes the request or passes it to the next in line.
  2. Bidirectional Chain: Handlers can traverse the chain in both forward and backward directions, allowing for more complex decision-making scenarios.
  3. Hierarchical Chain: Handlers are organized into a hierarchical structure, where certain handlers have sub-handlers. Requests can be passed down the hierarchy or propagated back up if necessary.
  4. Dynamic Chain: The chain’s composition can change dynamically during runtime, enabling on-the-fly adjustments to handle different types of requests.

These variations illustrate the adaptability of the Chain of Responsibility pattern, making it a versatile solution in various contexts.

Basic Implementation in Python

Next, we’ll delve into implementing the Chain of Responsibility pattern in Python, taking each step to create a basic example. Following that, we’ll explore a more pragmatic scenario: Middleware usage in web frameworks.

Step 1: Define the Handler Interface

In this step, we define an abstract base class Handler with a method handle_request. All concrete handlers will implement this method.

from abc import ABC, abstractmethod

class Handler(ABC):
@abstractmethod
def handle_request(self, request):
pass

Step 2: Create Concrete Handlers

We create two concrete handlers, ConcreteHandlerA and ConcreteHandlerB, which implements the handle_request method to process or pass requests.

class ConcreteHandlerA(Handler):
def handle_request(self, request):
if request == 'A':
print("Handled by Handler A")
else:
print("Passed to the parent handler")
super().handle_request(request)

class ConcreteHandlerB(Handler):
def handle_request(self, request):
if request == 'B':
print("Handled by Handler B")
else:
print("Passed to the parent handler")
super().handle_request(request)

Step 3: Create the Chain

The Chain class manages a list of handlers and provides a method to add handlers and handle requests in sequence.

class Chain:
def __init__(self):
self.handlers = []

def add_handler(self, handler):
self.handlers.append(handler)

def handle_request(self, request):
for handler in self.handlers:
handler.handle_request(request)

Step 4: Client Code

if __name__ == "__main__":
chain = Chain()
chain.add_handler(ConcreteHandlerA())
chain.add_handler(ConcreteHandlerB())

requests = ['A', 'B', 'C']

for request in requests:
print(f"Processing request: {request}")
chain.handle_request(request)
print()

Middleware Implementation Using Chain of Responsibility

In this code, we are implementing the Chain of Responsibility design pattern in Python to showcase a practical use case: middleware in web frameworks. The code is organized into three sections.

Step 1: Abstract Middleware Base

We Define an abstract base class Middleware that serves as the foundation for concrete middleware classes.

from abc import ABC, abstractmethod

# Section 1: Abstract Middleware Base

class Middleware(ABC):
"""Abstract Middleware class serving as the base of the chain."""

@abstractmethod
def handle_request(self, request):
"""Handle the request; must be implemented by concrete classes."""
pass

Step 2: Concrete Middleware Implementations

We Implement concrete middleware classes (AuthenticationMiddleware, LoggingMiddleware, DataValidationMiddleware) that represent different stages of request processing.

Each middleware can handle specific tasks like authentication, logging, or data validation and can pass the request to the next middleware in the chain.

# Section 2: Concrete Middleware Implementations

class AuthenticationMiddleware(Middleware):
"""Middleware responsible for user authentication."""

def handle_request(self, request):
"""Handle authentication or pass to the next middleware in the chain."""
if self.authenticate(request):
print("Authentication middleware: Authenticated successfully")
# Pass the request to the next middleware or handler in the chain.
return super().handle_request(request)
else:
print("Authentication middleware: Authentication failed")
# Stop the chain if authentication fails.
return None

def authenticate(self, request):
"""Implement authentication logic here."""
# Return True if authentication is successful, else False.
pass

class LoggingMiddleware(Middleware):
"""Middleware responsible for logging requests."""

def handle_request(self, request):
"""Handle request logging and pass to the next middleware in the chain."""
print("Logging middleware: Logging request")
# Pass the request to the next middleware or handler in the chain.
return super().handle_request(request)

class DataValidationMiddleware(Middleware):
"""Middleware responsible for data validation."""

def handle_request(self, request):
"""Handle data validation or pass to the next middleware in the chain."""
if self.validate_data(request):
print("Data Validation middleware: Data is valid")
# Pass the request to the next middleware or handler in the chain.
return super().handle_request(request)
else:
print("Data Validation middleware: Invalid data")
# Stop the chain if data validation fails.
return None

def validate_data(self, request):
"""Implement data validation logic here."""
# Return True if data is valid, else False.
pass

Section 3: Request Handling Class and Client Code

We implement the RequestHandler class responsible for the final request processing and provides client code to create and configure the middleware chain.

# Section 3: Request Handling Class and Client Code

# Chain class to handle the final request and manage middleware.
class Chain:
def __init__(self):
self.middlewares = []

def add_middleware(self, middleware):
self.middlewares.append(middleware)

def handle_request(self, request):
for middleware in self.middlewares:
request = middleware.handle_request(request)
if request is None:
print("Request processing stopped.")
break

# Client code to create and configure the middleware chain.
if __name__ == "__main__":
# Create middleware instances.
auth_middleware = AuthenticationMiddleware()
logging_middleware = LoggingMiddleware()
data_validation_middleware = DataValidationMiddleware()

# Create the chain and add middleware.
chain = Chain()
chain.add_middleware(auth_middleware)
chain.add_middleware(logging_middleware)
chain.add_middleware(data_validation_middleware)

# Simulate an HTTP request.
http_request = {"user": "username", "data": "valid_data"}
chain.handle_request(http_request)

GitHub Repo 🎉

Explore all code examples and design pattern implementations on GitHub!

Real-world Use Cases: Chain of Responsibility in Action

The Chain of Responsibility Design Pattern is practical and applicable in various real-world scenarios. Here are programming examples showcasing its effectiveness:

  1. Event Handling in User Interfaces: In graphical user interfaces (GUIs), events like mouse clicks or keypresses can pass through a chain of event handlers. Each handler decides whether to handle the event or pass it to the next handler.
  2. Middleware in Web Frameworks: Web frameworks often use middleware to process HTTP requests. Middleware components form a chain, where each component can perform tasks like authentication, logging, or data validation.
  3. Logging Systems: In logging frameworks, loggers can be organized into a chain. Each logger can filter and handle log messages based on their severity level, ensuring efficient logging.
  4. Request Handling in Web Servers: In web servers, incoming HTTP requests can pass through a chain of request handlers. Handlers may perform tasks like routing, authentication, and data processing.
  5. Exception Handling in Software Layers: Exception handling can be implemented using the Chain of Responsibility pattern. Different handlers can catch and handle exceptions at various levels, enhancing error management.
  6. Financial Transaction Processing: In financial systems, transaction processing often involves multiple validation stages. Each stage in the chain verifies the transaction for accuracy and compliance.
  7. Workflow Automation: Workflow systems can use the Chain of Responsibility pattern to manage complex processes. Each step in the workflow is handled by a different component in the chain.
  8. Content Filters in Email Clients: Email clients can employ content filters in a chain to categorize and manage incoming emails, such as spam filters, categorization, and prioritization.
  9. Data Transformation Pipelines: Data transformation pipelines can consist of a series of processors in a chain. Each processor applies a specific transformation to data before passing it to the next.
  10. Game Character Behaviors: In game development, character behaviors can be modeled using the Chain of Responsibility. Each behavior in the chain defines how the character responds to different game events.
Dall-E generated image with the following concept: Puzzle pieces fitting together, with each piece representing a handler

Advantages

The Chain of Responsibility pattern offers several advantages:

  1. Flexibility: You can easily add, remove, or reorder handlers in the chain without affecting the client code. This flexibility allows for dynamic configuration of the processing flow.
  2. Single Responsibility Principle: Each handler has a single responsibility, making the code easier to understand and maintain. This adheres to the Single Responsibility Principle (SRP).
  3. Decoupling: The sender and receiver are decoupled, reducing dependencies and making the code more modular and testable.

Considerations and Potential Drawbacks

While the Chain of Responsibility pattern offers many benefits, it also has some potential downsides:

  1. Risk of Unhandled Requests: If no handler in the chain can process a request, it remains unhandled. Careful design is needed to ensure that all requests are handled appropriately.
  2. Performance Overhead: Passing requests through a chain of handlers can introduce a slight performance overhead due to the dynamic nature of the pattern.
Stacked blocks of varying sizes and colors, each block symbolizing a handler
Dall-E generated image with the following concept: Stacked blocks of varying sizes and colors, each block symbolizing a handler

Relations with Other Patterns — TL;DR;

The Chain of Responsibility pattern exhibits unique characteristics that distinguish it from other design patterns. Let’s compare it with the Command, Mediator, and Observer patterns to highlight their differences:

Chain of Responsibility vs. Command Pattern

While both patterns deal with requests, the Chain of Responsibility focuses on routing a request through a series of handlers, whereas the Command Pattern is about encapsulating a request as an object with a specific action.

  • Chain of Responsibility: Routes requests through a chain of handlers, focusing on decoupling senders and receivers.
  • Command: Encapsulates requests as objects, emphasizing the separation of sender and receiver, enabling queuing, logging, or undoing operations.

Chain of Responsibility vs. Mediator Pattern

The Chain of Responsibility is focused on the sequential processing of requests, while the Mediator pattern is concerned with centralizing and managing communication between objects.

  • Chain of Responsibility: Defines a linear chain of handlers for request processing with potential multiple handlers.
  • Mediator: Centralizes communication between objects, promoting loose coupling, and managing interactions.

Chain of Responsibility vs. Observer Pattern

While both patterns involve multiple objects, the Chain of Responsibility is focused on handling requests through a chain of handlers, whereas the Observer pattern deals with notifying dependent objects of changes in another object’s state.

  1. Chain of Responsibility: Passes requests through a handler chain, processing or delegating them.
  2. Observer: Establishes one-to-many dependencies for notifying changes in object state.

Conclusion

Design patterns are indispensable tools in the software engineer’s toolbox. The Chain of Responsibility pattern is a powerful choice when you need to create a flexible and extensible processing chain. By understanding its principles and applying them in Python, you can improve the maintainability and scalability of your software systems.

While it has advantages like flexibility and decoupling, be mindful of potential downsides, such as the risk of unhandled requests and slight performance overhead. Ultimately, the Chain of Responsibility pattern empowers you to design elegant and modular solutions to complex problems in the world of software engineering.

Hope you enjoyed the Chain of Responsibility pattern exploration 🙌 Happy coding! 👨‍💻

Next on the Series 🚀

Read More 📜

The Series 🧭

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software (Book)
  2. refactoring.guru Chain of Responsibility
  3. Head First Design Patterns (Book)
  4. Chain of Responsibility
  5. Chain of Responsibility — Python Design Patterns

--

--

Amir Lavasani

I delve into machine learning 🤖 and software architecture 🏰 to enhance my expertise while sharing insights with Medium readers. 📃