Chain of Responsibility Design Pattern Tutorial
Introduction
The Chain of Responsibility Design Pattern is a behavioral design pattern that allows you to pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
This pattern:
- Decouples the sender of a request from its receivers
- Allows multiple objects to handle the request without the sender knowing which object will ultimately handle it
- Enables you to add or remove handlers dynamically
- Promotes the Single Responsibility Principle by allowing each handler to focus on one type of processing
At its core, the Chain of Responsibility pattern creates a chain of receiver objects for a request. This pattern gives more than one object a chance to handle the request, and the request is passed along the chain until an object handles it.
Real-World Analogy: Customer Support System
Imagine you’re calling a company’s customer support:
- Level 1 Support (First Handler): Answers basic questions
- If they can solve your problem → Done!
- If not → Escalates to Level 2
- Level 2 Support (Second Handler): Handles more complex issues
- If they can solve your problem → Done!
- If not → Escalates to Level 3
- Level 3 Support (Third Handler): Handles technical/specialized issues
- If they can solve your problem → Done!
- If not → Escalates to Manager
- Manager (Final Handler): Makes executive decisions
- Handles the issue or provides alternative solutions
The key insight here is that you don’t need to know who will ultimately solve your problem. You just start at the beginning of the chain, and the request automatically flows to the right person.
Benefits of this approach:
- You don’t need to know the entire support structure
- New support levels can be added without changing the process
- Each level focuses on what they do best
- Issues are automatically routed to the appropriate handler
Chain of Responsibility Pattern Structure
The Chain of Responsibility pattern typically consists of:
- Handler: An interface or abstract class that defines a method for handling requests and optionally a method for setting the next handler
- Concrete Handlers: Implement the Handler interface and handle requests they’re responsible for
- Client: Initiates the request to a handler in the chain
- Chain: The linked structure of handlers
Python Implementation
Let’s implement a simple example of the Chain of Responsibility pattern in Python:
from abc import ABC, abstractmethod
from typing import Optional
# Handler interface
class Handler(ABC):
def __init__(self):
self._next_handler: Optional[Handler] = None
def set_next(self, handler: 'Handler') -> 'Handler':
"""Set the next handler in the chain"""
self._next_handler = handler
return handler # Return handler for chaining
@abstractmethod
def handle(self, request: str) -> Optional[str]:
"""Handle the request or pass it to the next handler"""
pass
# Concrete Handlers
class Level1Support(Handler):
def handle(self, request: str) -> Optional[str]:
if "password reset" in request.lower():
return f"Level 1 Support: Password reset link sent to your email."
elif self._next_handler:
return self._next_handler.handle(request)
return None
class Level2Support(Handler):
def handle(self, request: str) -> Optional[str]:
if "billing" in request.lower() or "payment" in request.lower():
return f"Level 2 Support: Billing issue resolved. Refund processed."
elif self._next_handler:
return self._next_handler.handle(request)
return None
class Level3Support(Handler):
def handle(self, request: str) -> Optional[str]:
if "technical" in request.lower() or "bug" in request.lower():
return f"Level 3 Support: Technical issue logged. Engineering team notified."
elif self._next_handler:
return self._next_handler.handle(request)
return None
class Manager(Handler):
def handle(self, request: str) -> Optional[str]:
# Manager handles everything that reaches them
return f"Manager: I'll personally handle your request: '{request}'"
# Client code
if __name__ == "__main__":
# Create handlers
level1 = Level1Support()
level2 = Level2Support()
level3 = Level3Support()
manager = Manager()
# Build the chain
level1.set_next(level2).set_next(level3).set_next(manager)
# Test different requests
requests = [
"I need a password reset",
"I have a billing question about my last payment",
"There's a technical bug in the application",
"I want to speak to someone about a refund"
]
for request in requests:
print(f"\nRequest: {request}")
response = level1.handle(request)
print(f"Response: {response}")
Output:
Request: I need a password reset
Response: Level 1 Support: Password reset link sent to your email.
Request: I have a billing question about my last payment
Response: Level 2 Support: Billing issue resolved. Refund processed.
Request: There's a technical bug in the application
Response: Level 3 Support: Technical issue logged. Engineering team notified.
Request: I want to speak to someone about a refund
Response: Manager: I'll personally handle your request: 'I want to speak to someone about a refund'
More Complex Example: Expense Approval System
Let’s implement a more sophisticated example with an expense approval system:
from abc import ABC, abstractmethod
from typing import Optional
from enum import Enum
class ExpenseType(Enum):
TRAVEL = "travel"
EQUIPMENT = "equipment"
TRAINING = "training"
ENTERTAINMENT = "entertainment"
OTHER = "other"
class Expense:
def __init__(self, amount: float, expense_type: ExpenseType, description: str):
self.amount = amount
self.expense_type = expense_type
self.description = description
def __str__(self):
return f"{self.expense_type.value.title()} expense: ${self.amount:.2f} - {self.description}"
# Handler interface
class Approver(ABC):
def __init__(self, name: str):
self.name = name
self._next_approver: Optional[Approver] = None
def set_next(self, approver: 'Approver') -> 'Approver':
self._next_approver = approver
return approver
@abstractmethod
def can_approve(self, expense: Expense) -> bool:
"""Determine if this approver can handle the expense"""
pass
def approve(self, expense: Expense) -> str:
"""Process the expense approval"""
if self.can_approve(expense):
return self._process_approval(expense)
elif self._next_approver:
return self._next_approver.approve(expense)
else:
return f"❌ Expense rejected: {expense} - Exceeds all approval limits"
@abstractmethod
def _process_approval(self, expense: Expense) -> str:
"""Process the approval at this level"""
pass
# Concrete Handlers
class TeamLead(Approver):
def __init__(self, name: str):
super().__init__(name)
self.approval_limit = 1000
def can_approve(self, expense: Expense) -> bool:
return expense.amount <= self.approval_limit
def _process_approval(self, expense: Expense) -> str:
return f"✅ Approved by Team Lead {self.name}: {expense}"
class Manager(Approver):
def __init__(self, name: str):
super().__init__(name)
self.approval_limit = 5000
def can_approve(self, expense: Expense) -> bool:
return expense.amount <= self.approval_limit
def _process_approval(self, expense: Expense) -> str:
return f"✅ Approved by Manager {self.name}: {expense}"
class Director(Approver):
def __init__(self, name: str):
super().__init__(name)
self.approval_limit = 20000
def can_approve(self, expense: Expense) -> bool:
return expense.amount <= self.approval_limit
def _process_approval(self, expense: Expense) -> str:
return f"✅ Approved by Director {self.name}: {expense}"
class CEO(Approver):
def __init__(self, name: str):
super().__init__(name)
self.approval_limit = 100000
def can_approve(self, expense: Expense) -> bool:
return expense.amount <= self.approval_limit
def _process_approval(self, expense: Expense) -> str:
return f"✅ Approved by CEO {self.name}: {expense}"
# Client code
if __name__ == "__main__":
# Create approval chain
team_lead = TeamLead("Alice")
manager = Manager("Bob")
director = Director("Carol")
ceo = CEO("David")
# Build the chain
team_lead.set_next(manager).set_next(director).set_next(ceo)
# Create various expenses
expenses = [
Expense(500, ExpenseType.EQUIPMENT, "New keyboard and mouse"),
Expense(2500, ExpenseType.TRAINING, "Conference registration"),
Expense(15000, ExpenseType.TRAVEL, "International business trip"),
Expense(75000, ExpenseType.EQUIPMENT, "New server infrastructure"),
Expense(150000, ExpenseType.OTHER, "Office renovation"),
]
# Process expenses
print("=== Expense Approval System ===\n")
for expense in expenses:
print(f"Submitting: {expense}")
result = team_lead.approve(expense)
print(f"{result}\n")
Output:
=== Expense Approval System ===
Submitting: Equipment expense: $500.00 - New keyboard and mouse
✅ Approved by Team Lead Alice: Equipment expense: $500.00 - New keyboard and mouse
Submitting: Training expense: $2500.00 - Conference registration
✅ Approved by Manager Bob: Training expense: $2500.00 - Conference registration
Submitting: Travel expense: $15000.00 - International business trip
✅ Approved by Director Carol: Travel expense: $15000.00 - International business trip
Submitting: Equipment expense: $75000.00 - New server infrastructure
✅ Approved by CEO David: Equipment expense: $75000.00 - New server infrastructure
Submitting: Other expense: $150000.00 - Office renovation
❌ Expense rejected: Other expense: $150000.00 - Office renovation - Exceeds all approval limits
Advanced Example: Logging System with Multiple Handlers
from abc import ABC, abstractmethod
from typing import Optional
from enum import Enum
from datetime import datetime
class LogLevel(Enum):
DEBUG = 1
INFO = 2
WARNING = 3
ERROR = 4
CRITICAL = 5
class LogMessage:
def __init__(self, level: LogLevel, message: str):
self.level = level
self.message = message
self.timestamp = datetime.now()
def __str__(self):
return f"[{self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}] [{self.level.name}] {self.message}"
# Handler interface
class Logger(ABC):
def __init__(self, level: LogLevel):
self.level = level
self._next_logger: Optional[Logger] = None
def set_next(self, logger: 'Logger') -> 'Logger':
self._next_logger = logger
return logger
def log(self, message: LogMessage):
"""Process the log message"""
if message.level.value >= self.level.value:
self._write(message)
# Always pass to next logger in chain
if self._next_logger:
self._next_logger.log(message)
@abstractmethod
def _write(self, message: LogMessage):
"""Write the log message"""
pass
# Concrete Handlers
class ConsoleLogger(Logger):
def __init__(self, level: LogLevel):
super().__init__(level)
def _write(self, message: LogMessage):
print(f"[CONSOLE] {message}")
class FileLogger(Logger):
def __init__(self, level: LogLevel, filename: str):
super().__init__(level)
self.filename = filename
def _write(self, message: LogMessage):
with open(self.filename, 'a') as f:
f.write(f"{message}\n")
print(f"[FILE] Logged to {self.filename}: {message.level.name}")
class EmailLogger(Logger):
def __init__(self, level: LogLevel, email: str):
super().__init__(level)
self.email = email
def _write(self, message: LogMessage):
# Simulate sending email
print(f"[EMAIL] Sent to {self.email}: {message.level.name} - {message.message}")
class DatabaseLogger(Logger):
def __init__(self, level: LogLevel):
super().__init__(level)
def _write(self, message: LogMessage):
# Simulate database write
print(f"[DATABASE] Stored: {message}")
# Client code
if __name__ == "__main__":
# Create loggers with different levels
console_logger = ConsoleLogger(LogLevel.DEBUG)
file_logger = FileLogger(LogLevel.WARNING, "app.log")
email_logger = EmailLogger(LogLevel.ERROR, "admin@example.com")
db_logger = DatabaseLogger(LogLevel.CRITICAL)
# Build the chain
console_logger.set_next(file_logger).set_next(email_logger).set_next(db_logger)
# Create log messages
messages = [
LogMessage(LogLevel.DEBUG, "Application started"),
LogMessage(LogLevel.INFO, "User logged in"),
LogMessage(LogLevel.WARNING, "High memory usage detected"),
LogMessage(LogLevel.ERROR, "Failed to connect to database"),
LogMessage(LogLevel.CRITICAL, "System crash imminent"),
]
# Process log messages
print("=== Multi-Level Logging System ===\n")
for msg in messages:
print(f"\nProcessing: {msg.level.name} message")
console_logger.log(msg)
print()
How Chain of Responsibility Handles OOP Principles
The Chain of Responsibility pattern exemplifies several key OOP principles:
1. Single Responsibility Principle
- Each handler has a single responsibility: handling a specific type of request
- Handlers don’t need to know about other handlers in the chain
- Each handler focuses on its own logic
2. Open/Closed Principle
- The system is open for extension (new handlers can be added)
- But closed for modification (existing handlers don’t need to change)
- New handlers can be inserted into the chain without modifying existing code
3. Loose Coupling
- The sender doesn’t need to know which handler will process the request
- Handlers don’t need to know about each other (only the next handler)
- Reduces dependencies between components
4. Flexibility
- The chain can be modified at runtime
- Handlers can be added, removed, or reordered dynamically
- Different chains can be created for different scenarios
5. Encapsulation
- Each handler encapsulates its own processing logic
- The chain structure is hidden from the client
- Implementation details are not exposed
Pain Points Without the Chain of Responsibility Pattern
Without the Chain of Responsibility pattern, you might encounter these issues:
1. Tight Coupling
Without Chain of Responsibility:
class RequestProcessor:
def __init__(self):
self.level1 = Level1Support()
self.level2 = Level2Support()
self.level3 = Level3Support()
self.manager = Manager()
def process_request(self, request):
if self.level1.can_handle(request):
return self.level1.handle(request)
elif self.level2.can_handle(request):
return self.level2.handle(request)
elif self.level3.can_handle(request):
return self.level3.handle(request)
else:
return self.manager.handle(request)
Problems:
- The processor is tightly coupled to all handlers
- Adding a new handler requires modifying the processor
- The order of handlers is hardcoded
- Can’t easily change the chain at runtime
2. Violation of Open/Closed Principle
- Adding new handlers requires modifying existing code
- Can’t extend the system without changing the core logic
- Increases risk of introducing bugs
3. Complex Conditional Logic
- Multiple if-else statements to determine which handler to use
- Difficult to maintain as the number of handlers grows
- Hard to understand the flow of request processing
4. Lack of Flexibility
- Can’t easily reorder handlers
- Can’t add or remove handlers dynamically
- Different scenarios require different code paths
5. Testing Challenges
- Hard to test handlers in isolation
- Can’t easily mock or substitute handlers
- Difficult to test different chain configurations
When to Use the Chain of Responsibility Pattern
The Chain of Responsibility pattern is particularly useful when:
- Multiple objects can handle a request, and the handler isn’t known in advance
- You want to issue a request to one of several objects without specifying the receiver explicitly
- The set of handlers should be specified dynamically
- You want to avoid coupling the sender to the receiver
- You need to process requests in a specific order
- You want to add or remove responsibilities dynamically
Common Use Cases
- Event Handling Systems: GUI frameworks, event bubbling in DOM
- Logging Frameworks: Different log levels and outputs
- Authentication/Authorization: Multiple authentication methods
- Middleware in Web Frameworks: Request processing pipeline
- Approval Workflows: Multi-level approval systems
- Exception Handling: Try-catch blocks in different scopes
- Validation Chains: Multiple validation rules
Advantages
- Reduced Coupling: Sender doesn’t need to know the receiver
- Flexibility: Chain can be modified at runtime
- Single Responsibility: Each handler has one job
- Easy to Extend: New handlers can be added without changing existing code
- Dynamic Configuration: Chain structure can be changed dynamically
Disadvantages
- No Guarantee of Handling: Request might not be handled if no handler can process it
- Performance: Request might pass through many handlers before being processed
- Debugging Difficulty: Can be hard to trace which handler processed a request
- Chain Configuration: Incorrect chain setup can lead to requests not being handled
Best Practices
- Provide a Default Handler: Always have a handler at the end of the chain
- Keep Handlers Focused: Each handler should have a single, clear responsibility
- Document the Chain: Make it clear what each handler does and in what order
- Consider Performance: Be mindful of long chains that might impact performance
- Use Logging: Log when handlers process or pass requests for debugging
- Make Chains Configurable: Allow chains to be configured externally
Conclusion
The Chain of Responsibility Design Pattern is a powerful tool for creating flexible, maintainable systems where multiple objects can handle a request. It promotes loose coupling, makes it easy to add new handlers, and allows for dynamic configuration of the processing chain.
By passing requests along a chain of handlers, you can:
- Decouple senders from receivers
- Add or remove handlers without changing existing code
- Process requests in a specific order
- Handle requests at the appropriate level
- Create flexible, extensible systems
Understanding and applying the Chain of Responsibility pattern will help you write more maintainable, flexible, and robust code, especially in scenarios where multiple objects might handle a request or where the handling logic needs to be easily extensible.