[UPDATED 12/7/2023]
✨ GitHub repo with real-world use cases: https://github.com/herchila/design-patterns/tree/main/behavioral_patterns
Chain of Responsibility Pattern
Purpose: To pass a request along a chain of handlers. Upon receiving a request, each handler decides either to process it or to pass it to the next handler in the chain.
Example: A logging system that sends messages to different handlers (console, file, email) based on the message's severity.
class Handler:
def __init__(self, successor=None):
self._successor = successor
def handle(self, request):
res = self.check_request(request)
if not res and self._successor:
self._successor.handle(request)
def check_request(self, request): pass
class ConsoleHandler(Handler):
def check_request(self, request):
if request < 3:
print(f"Console: {request}")
return True
class FileHandler(Handler):
def check_request(self, request):
if 3 <= request < 5:
print(f"File: {request}")
return True
class EmailHandler(Handler):
def check_request(self, request):
if request >= 5:
print(f"Email: {request}")
return True
# Usage
chain = ConsoleHandler(FileHandler(EmailHandler()))
chain.handle(2) # Console: 2
chain.handle(4) # File: 4
chain.handle(5) # Email: 5
Real-world use case
** Scenario**:
Imagine a Customer Support System for a technology company that provides various services such as hardware repair, software troubleshooting, and account management. Customers can reach out with a wide range of issues, from simple password resets to complex hardware malfunctions. The system needs to handle these queries efficiently, ensuring that each issue is addressed by the most appropriate department. Without proper handling, queries could be misdirected, leading to delays and customer dissatisfaction.
Problem:
The challenge lies in efficiently categorizing and routing these customer queries to the right department. Handling this manually can be error-prone and inefficient. Moreover, as the company grows and the range of services expands, the system must be adaptable to handle new types of queries without major overhauls.
Solution:
The Chain of Responsibility pattern is ideal for this situation. It allows us to pass requests along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain.
class Handler:
"""Abstract Handler: inherited by all concrete handlers."""
def __init__(self, successor=None):
self._successor = successor
def handle_request(self, request):
if not self.can_handle(request):
if self._successor is not None:
self._successor.handle_request(request)
def can_handle(self, request):
raise NotImplementedError('Must provide implementation in subclass!')
class HardwareSupportHandler(Handler):
"""Concrete handler: handles hardware support requests."""
def can_handle(self, request):
return request == "Hardware Support"
class SoftwareSupportHandler(Handler):
"""Concrete handler: handles software support requests."""
def can_handle(self, request):
return request == "Software Support"
class AccountSupportHandler(Handler):
"""Concrete handler: handles account support requests."""
def can_handle(self, request):
return request == "Account Support"
# Create handlers
hardware_handler = HardwareSupportHandler()
software_handler = SoftwareSupportHandler(hardware_handler)
account_handler = AccountSupportHandler(software_handler)
# Client code
def client_code(handler, request):
print(f"Processing request for {request}")
handler.handle_request(request)
# Test our chain
client_code(account_handler, "Hardware Support")
client_code(account_handler, "Software Support")
client_code(account_handler, "Account Support")
In this code, we have a base Handler
class that defines the interface for handling requests. The HardwareSupportHandler
, SoftwareSupportHandler
, and AccountSupportHandler
are concrete handlers that process specific types of requests. We link these handlers in a chain (account -> software -> hardware). When a request comes in, it starts at the beginning of the chain (account support) and moves down the chain until it finds a handler that can process it.
Observer Pattern
Purpose: To define a one-to-many dependency between objects, where a change in one object results in automatic notifications and updates to its dependents.
Example: A weather station broadcasting temperature updates to multiple display devices.
class Observer:
def update(self, subject): pass
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self):
for observer in self._observers:
observer.update(self)
class WeatherStation(Subject):
_temperature = None
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, temp):
self._temperature = temp
self.notify()
class DisplayDevice(Observer):
def update(self, subject):
print(f"Temperature Update: {subject.temperature}°C")
# Usage
weather_station = WeatherStation()
display = DisplayDevice()
weather_station.attach(display)
weather_station.temperature = 26 # Temperature Update: 26°C
Real-world use case
Scenario:
Imagine an Online Auction System where users can bid on various items. In this system, bidders need to be constantly updated about the current highest bid so they can decide whether to place a higher bid. Each item has multiple bidders watching it, and they all need to receive updates whenever the bid changes. Managing these updates efficiently is crucial to ensure a smooth and responsive auction experience.
Problem:
The challenge is in notifying all interested bidders of the current highest bid without requiring them to constantly check the item manually. Manually updating each bidder would be inefficient and impractical, especially as the number of bidders and items increases. This could lead to a poor user experience, as bidders might miss out on placing higher bids due to delayed updates.
Solution:
The Observer Pattern provides an elegant solution to this problem. It allows multiple objects (observers) to watch another object (subject) and be notified automatically whenever there is a change in the subject. In the context of our online auction system, each item can be a subject, and the bidders can be observers.
class AuctionItem:
"""Subject: Auction Item being bid on."""
def __init__(self, name):
self._name = name
self._bidders = []
self._highest_bid = 0
def attach(self, bidder):
self._bidders.append(bidder)
def detach(self, bidder):
self._bidders.remove(bidder)
def notify(self):
for bidder in self._bidders:
bidder.update(self)
def receive_bid(self, bid_amount):
if bid_amount > self._highest_bid:
self._highest_bid = bid_amount
self.notify()
def get_highest_bid(self):
return self._highest_bid
class Bidder:
"""Observer: Bidders watching the auction item."""
def __init__(self, name):
self._name = name
def update(self, auction_item):
print(f"{self._name} has been notified. New highest bid: {auction_item.get_highest_bid()} on item {auction_item._name}")
# Client code
item = AuctionItem("Vintage Vase")
bidder1 = Bidder("Alice")
bidder2 = Bidder("Bob")
bidder3 = Bidder("Charlie")
item.attach(bidder1)
item.attach(bidder2)
item.attach(bidder3)
# Bids are received
item.receive_bid(100) # Updates all bidders
item.receive_bid(150) # Updates all bidders
In this implementation, AuctionItem
is the subject, and Bidder
is the observer. Each Bidder
instance is attached to an AuctionItem
. When a new bid is placed on the item (receive_bid
method), the item updates its highest bid and notifies all attached bidders by calling their update
method.
Strategy Pattern
Purpose: To define a family of algorithms, encapsulate each one, and make them interchangeable. The strategy pattern lets the algorithm vary independently from the clients that use it.
Example: Different sorting algorithms used in a context based on the size or complexity of the data set.
from typing import List
class SortingStrategy:
def sort(self, dataset: List[int]) -> List[int]: pass
class QuickSort(SortingStrategy):
def sort(self, dataset: List[int]) -> List[int]:
print("Sorting using quick sort")
return sorted(dataset) # Simplification for demonstration
class MergeSort(SortingStrategy):
def sort(self, dataset: List[int]) -> List[int]:
print("Sorting using merge sort")
return sorted(dataset) # Simplification for demonstration
class Sorter:
def __init__(self, strategy: SortingStrategy):
self._strategy = strategy
def sort(self, dataset: List[int]) -> List[int]:
return self._strategy.sort(dataset)
# Usage
dataset = [1, 5, 3, 4, 2]
sorter = Sorter(QuickSort())
sorter.sort(dataset) # Sorting using quick sort
sorter = Sorter(MergeSort())
sorter.sort(dataset) # Sorting using merge sort
Real-world use case
Scenario:
Imagine an E-commerce platform where different products are shipped to various locations worldwide. Shipping costs vary based on several factors such as destination, weight, and shipping speed. The platform needs a flexible way to calculate shipping costs that can adapt to various shipping strategies, such as standard, expedited, and international shipping. Each strategy involves different cost calculations.
Problem:
The main challenge is implementing a shipping cost calculation system that is adaptable and maintainable. Hard-coding each shipping method's cost calculation into the main application can lead to a bloated codebase, making it difficult to add or modify shipping methods in the future. As the business grows and shipping requirements evolve, this inflexibility can become a significant hindrance.
Solution:
The Strategy Pattern offers a solution by defining a family of algorithms, encapsulating each one, and making them interchangeable. The strategy pattern lets the algorithm vary independently from clients that use it. In the context of our E-commerce platform, we can define different shipping strategies as separate algorithms and switch between them as needed.
from abc import ABC, abstractmethod
class ShippingStrategy(ABC):
"""Abstract base class for shipping strategies."""
@abstractmethod
def calculate(self, order):
pass
class StandardShipping(ShippingStrategy):
"""Concrete strategy for standard shipping."""
def calculate(self, order):
return 5.00 # Flat rate
class ExpeditedShipping(ShippingStrategy):
"""Concrete strategy for expedited shipping."""
def calculate(self, order):
return 10.00 # Flat rate
class InternationalShipping(ShippingStrategy):
"""Concrete strategy for international shipping."""
def calculate(self, order):
return 20.00 # Flat rate
class Order:
"""Client class that uses a shipping strategy."""
def __init__(self, strategy: ShippingStrategy):
self.strategy = strategy
def calculate_shipping_cost(self):
return self.strategy.calculate(self)
# Usage
# Client code
standard_order = Order(StandardShipping())
print(f"Standard Shipping Cost: {standard_order.calculate_shipping_cost()}")
expedited_order = Order(ExpeditedShipping())
print(f"Expedited Shipping Cost: {expedited_order.calculate_shipping_cost()}")
international_order = Order(InternationalShipping())
print(f"International Shipping Cost: {international_order.calculate_shipping_cost()}")
In this implementation, ShippingStrategy
is an abstract base class that defines a method calculate
. Classes StandardShipping
, ExpeditedShipping
, and InternationalShipping
implement this method to calculate shipping costs according to their respective strategies. The Order
class, which represents a customer order, uses a ShippingStrategy
to calculate its shipping cost.
Visitor Pattern
Purpose: To separate an algorithm from the object structure on which it operates. It allows adding new operations to existing object structures without modifying them.
Example: Performing different operations on a set of objects representing different elements of an HTML document.
class HtmlElement:
def accept(self, visitor): pass
class HtmlParagraph(HtmlElement):
def accept(self, visitor):
visitor.visit_paragraph(self)
class HtmlAnchor(HtmlElement):
def accept(self, visitor):
visitor.visit_anchor(self)
class Visitor:
def visit_paragraph(self, paragraph): pass
def visit_anchor(self, anchor): pass
class RenderVisitor(Visitor):
def visit_paragraph(self, paragraph):
print("<p>Paragraph content</p>")
def visit_anchor(self, anchor):
print("<a href='url'>Anchor text</a>")
# Usage
# Client code
paragraph = HtmlParagraph()
anchor = HtmlAnchor()
render_visitor = RenderVisitor()
paragraph.accept(render_visitor)
anchor.accept(render_visitor)
In this implementation, the visit_paragraph
method in RenderVisitor
prints a simple HTML paragraph element, and visit_anchor prints an anchor (link) element. This is just a basic representation, and you can modify the content and attributes of the HTML elements as needed for your application.
The client code at the end creates instances of HtmlParagraph
and HtmlAnchor
, and then it uses the RenderVisitor
to "visit" these elements, which results in printing their HTML representations.
Real-world use case
Scenario:
Consider a scenario in a Customer Support System where different types of customer queries, like Technical Support, Billing, and General Inquiries, are handled. The system needs to generate various reports based on these queries, such as performance analysis, query type breakdown, and customer feedback. Given the diverse nature of the data and the different types of reports required, the system needs a flexible way to handle these reporting tasks without altering the existing query classes whenever a new report is needed.
Problem:
The challenge is to create a reporting system that can process a variety of customer query types and generate different reports without constantly modifying the query classes. Adding reporting functionalities directly to the query classes would violate the Single Responsibility Principle and lead to a rigid design that's hard to maintain and update, especially when new types of reports are required.
Solution:
The Visitor Pattern provides an elegant solution to this problem. It allows adding new operations to existing object structures without modifying them. The pattern involves a visitor class that performs operations on elements of an object structure. This way, you can add new reporting capabilities by simply adding new visitor classes.
from abc import ABC, abstractmethod
# Element Interface
class CustomerQuery(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Concrete Elements
class TechnicalSupportQuery(CustomerQuery):
def accept(self, visitor):
visitor.visit_technical_support(self)
class BillingQuery(CustomerQuery):
def accept(self, visitor):
visitor.visit_billing(self)
class GeneralInquiry(CustomerQuery):
def accept(self, visitor):
visitor.visit_general_inquiry(self)
# Visitor Interface
class ReportVisitor(ABC):
@abstractmethod
def visit_technical_support(self, query):
pass
@abstractmethod
def visit_billing(self, query):
pass
@abstractmethod
def visit_general_inquiry(self, query):
pass
# Concrete Visitors
class PerformanceReportVisitor(ReportVisitor):
def visit_technical_support(self, query):
# Perform operations specific to performance report for technical support
pass
def visit_billing(self, query):
# Perform operations specific to performance report for billing
pass
def visit_general_inquiry(self, query):
# Perform operations specific to performance report for general inquiries
pass
class FeedbackReportVisitor(ReportVisitor):
def visit_technical_support(self, query):
# Perform operations specific to feedback report for technical support
pass
def visit_billing(self, query):
# Perform operations specific to feedback report for billing
pass
def visit_general_inquiry(self, query):
# Perform operations specific to feedback report for general inquiries
pass
#Usage
# Client code
technical_query = TechnicalSupportQuery()
billing_query = BillingQuery()
general_query = GeneralInquiry()
performance_report_visitor = PerformanceReportVisitor()
feedback_report_visitor = FeedbackReportVisitor()
technical_query.accept(performance_report_visitor)
billing_query.accept(feedback_report_visitor)
In this implementation, CustomerQuery
is an interface for different types of customer queries, and ReportVisitor
is an interface for different types of report visitors. Concrete query classes like TechnicalSupportQuery
, BillingQuery
, and GeneralInquiry
implement the CustomerQuery
interface. Concrete visitor classes like PerformanceReportVisitor
and FeedbackReportVisitor
implement the ReportVisitor
interface to handle specific reporting tasks.
Top comments (0)