The Chain of Responsibility Pattern in Python
A powerful way to handle requests
This story is part of the “Design Patterns” series. You can find the other stories of this series here:
You can also find all the code used through this series on GitHub.
The Chain of Responsibility pattern is a design pattern in which a series of objects are chained together and a request is passed through the chain until an object is able to handle the request.
The objects in the chain are not aware of the structure of the chain, and the request is passed along until it reaches an object that can handle it.
Problems the Chain of Responsibility can Solve
Imagine we’re building a software that processes a list of orders.
Each order has a priority level (high, medium, low), and depending on the priority level, the order should be handled by different departments (sales, support, billing).
Without using the Chain of Responsibility pattern, we could use a series of if-else statements that check the priority level of each order and routes it to the appropriate department.
from unittest.mock import Mock
sales_department = Mock()
sales_department.handle_order = Mock(side_effect=lambda order: print(f"Sales department handling order {order}"))
support_department = Mock()
support_department.handle_order = Mock(side_effect=lambda order: print(f"Support department handling order {order}"))
billing_department = Mock()
billing_department.handle_order = Mock(side_effect=lambda order: print(f"Billing department handling order {order}"))
def process_order(order):
if order["priority"] == "high":
sales_department.handle_order(order)
elif order["priority"] == "medium":
support_department.handle_order(order)
elif order["priority"] == "low":
billing_department.handle_order(order)
if __name__ == "__main__":
order = {"priority": "low"}
process_order(order)Output:
Billing department handling order {'priority': 'low'}You already know this design is bad. Indeed, a series of conditional expressions is rarely a good thing. Here are some drawbacks of this design:
- It’s hard to extend and maintain, as adding a new priority level would require modifying the existing function, and adding a new condition for that priority level
- It’s not clear where the responsibility of handling each priority level lies
- It’s not reusable
Solution
To solve the problem using the Chain of Responsibility pattern, we can create a base class OrderHandler that defines the next handler in the chain and the method for handling the order.
class OrderHandler:
def __init__(self, next_handler=None):
self._next_handler = next_handler
def handle_order(self, order):
if self._handle_order(order):
return
if self._next_handler:
self._next_handler.handle_order(order)
def _handle_order(self, order):
raise NotImplementedErrorWe could even make this class abstract.
Then we can create a subclass for each priority level, that will inherit from OrderHandler and implement the _handle_order method to route the order to the appropriate department.
class HighOrderHandler(OrderHandler):
def _handle_order(self, order):
if order["priority"] == "high":
sales_department.handle_order(order)
return True
return False
class MediumOrderHandler(OrderHandler):
def _handle_order(self, order):
if order["priority"] == "medium":
support_department.handle_order(order)
return True
return False
class LowOrderHandler(OrderHandler):
def _handle_order(self, order):
if order["priority"] == "low":
billing_department.handle_order(order)
return True
return FalseIn the client code, we can link the instances together.
order_handler = HighOrderHandler(MediumOrderHandler(LowOrderHandler()))This design allows to add new priority level handlers without modifying the existing code, it’s easy to test, it’s clear where the responsibility of handling each priority level lies, and it’s reusable.
Maybe you think it adds complexity, but the fact that now we can add priority levels without touching the handling process is something very powerful.
Applications
- When a request can be handled by multiple objects and the sender of the request doesn’t know which object will handle it.
- When you want to decouple the sender of a request from the objects that handle the request.
- When you want to be able to add or remove handlers for a request dynamically.
Advantages
- It allows multiple objects to handle a request, which makes it more flexible than a single object handling all requests.
- It allows the sender of a request to be decoupled from the objects that handle the request.
- It makes it easy to add or remove handlers for a request dynamically.
- It makes the code more organized and clear by separating the responsibility of handling different types of requests.
Drawbacks
- It can make the code more complex, especially when there are many handlers in the chain.
- It can make it harder to trace the flow of a request through the chain of handlers.
- It can make it harder to identify the source of an error if the request is not handled correctly.
Final Note
From my experience, I use this pattern rarely, but it can be useful in some cases such as handling different types of requests in a web application, where each type of request can be handled by its own class.
To explore the other stories of this story, click below!
To explore more of my Python stories, click here! You can also access all my content by checking this page.
If you want to be notified every time I publish a new story, subscribe to me via email by clicking here!
If you’re not subscribed to medium yet and wish to support me or get access to all my stories, you can use my link:
