Table of Contents
-
- Intent
- Problem & Solution
- Python Implementation
- Use Cases
- Pros & Cons
-
- Intent
- Problem & Solution
- Python Implementation
- Use Cases
- Pros & Cons
-
- Intent
- Problem & Solution
- Python Implementation
- Use Cases
- Pros & Cons
-
- Intent
- Problem & Solution
- Python Implementation
- Use Cases
- Pros & Cons
-
- Intent
- Problem & Solution
- Python Implementation
- Use Cases
- Pros & Cons
-
- Intent
- Problem & Solution
- Python Implementation
- Use Cases
- Pros & Cons
1. Observer Pattern
Intent
Define a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (the observers) are notified and updated automatically.
Problem & Solution
Problem: You need multiple objects to react to changes in another object’s state (e.g., a weather station updating displays for temperature, humidity, and pressure). Tightly coupling the subject to its observers would make the code rigid and hard to extend.
Solution: Decouple the subject from observers using an interface. The subject maintains a list of observers and notifies them of state changes. Observers register/unregister themselves with the subject and define how to react to updates.
Python Implementation
Let’s implement a weather monitoring system where a WeatherStation (subject) notifies TemperatureDisplay and HumidityDisplay (observers) of state changes.
from abc import ABC, abstractmethod
# Observer Interface
class Observer(ABC):
@abstractmethod
def update(self, temperature: float, humidity: float) -> None:
pass
# Subject Interface
class Subject(ABC):
@abstractmethod
def attach(self, observer: Observer) -> None:
pass
@abstractmethod
def detach(self, observer: Observer) -> None:
pass
@abstractmethod
def notify(self) -> None:
pass
# Concrete Subject: Weather Station
class WeatherStation(Subject):
def __init__(self):
self._observers: list[Observer] = []
self._temperature: float = 0.0
self._humidity: float = 0.0
def attach(self, observer: Observer) -> None:
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify(self) -> None:
for observer in self._observers:
observer.update(self._temperature, self._humidity)
# Update weather data and trigger notifications
def set_measurements(self, temperature: float, humidity: float) -> None:
self._temperature = temperature
self._humidity = humidity
self.notify()
# Concrete Observers
class TemperatureDisplay(Observer):
def update(self, temperature: float, humidity: float) -> None:
print(f"Temperature Display: Current Temp = {temperature}°C")
class HumidityDisplay(Observer):
def update(self, temperature: float, humidity: float) -> None:
print(f"Humidity Display: Current Humidity = {humidity}%")
# Usage
if __name__ == "__main__":
weather_station = WeatherStation()
temp_display = TemperatureDisplay()
humidity_display = HumidityDisplay()
weather_station.attach(temp_display)
weather_station.attach(humidity_display)
# Simulate new weather data
weather_station.set_measurements(25.5, 60.0) # Both displays update
weather_station.set_measurements(27.0, 55.0) # Both displays update again
# Detach humidity display
weather_station.detach(humidity_display)
weather_station.set_measurements(28.0, 50.0) # Only temperature display updates
Output:
Temperature Display: Current Temp = 25.5°C
Humidity Display: Current Humidity = 60.0%
Temperature Display: Current Temp = 27.0°C
Humidity Display: Current Humidity = 55.0%
Temperature Display: Current Temp = 28.0°C
Use Cases
- Event-driven systems (e.g., GUI frameworks like Tkinter, where buttons notify listeners of clicks).
- Notifications (e.g., email/SMS alerts triggered by system events).
- Real-time data dashboards (e.g., stock price trackers).
Pros & Cons
| Pros | Cons |
|---|---|
| Loose coupling between subject and observers. | Observers may receive unnecessary updates. |
| Easy to add/remove observers dynamically. | Can lead to memory leaks if observers are not detached. |
2. Strategy Pattern
Intent
Define a family of interchangeable algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Problem & Solution
Problem: You need to offer multiple ways to perform an action (e.g., different payment methods: credit card, PayPal, Bitcoin). Hardcoding all methods into a single class would make it bloated and hard to extend.
Solution: Encapsulate each algorithm (strategy) in a separate class with a common interface. The client delegates the work to a strategy object, allowing runtime switching of strategies.
Python Implementation
Let’s implement a payment processor that supports credit card and PayPal payments.
from abc import ABC, abstractmethod
# Strategy Interface
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> None:
pass
# Concrete Strategies
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str, name: str):
self.card_number = card_number
self.name = name
def pay(self, amount: float) -> None:
print(f"Paid ${amount} via Credit Card (Card: {self.card_number}, Name: {self.name})")
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> None:
print(f"Paid ${amount} via PayPal (Email: {self.email})")
# Context: Uses a strategy to perform payment
class ShoppingCart:
def __init__(self, payment_strategy: PaymentStrategy):
self._payment_strategy = payment_strategy
self._items: list[str] = []
def add_item(self, item: str) -> None:
self._items.append(item)
def set_payment_strategy(self, payment_strategy: PaymentStrategy) -> None:
self._payment_strategy = payment_strategy
def checkout(self, amount: float) -> None:
print(f"Checking out items: {', '.join(self._items)}")
self._payment_strategy.pay(amount)
# Usage
if __name__ == "__main__":
# Initialize cart with Credit Card payment
cart = ShoppingCart(CreditCardPayment("4111-1111-1111-1111", "John Doe"))
cart.add_item("Laptop")
cart.add_item("Mouse")
cart.checkout(1200.0) # Pays with Credit Card
# Switch to PayPal payment
cart.set_payment_strategy(PayPalPayment("[email protected]"))
cart.add_item("Keyboard")
cart.checkout(150.0) # Pays with PayPal
Output:
Checking out items: Laptop, Mouse
Paid $1200.0 via Credit Card (Card: 4111-1111-1111-1111, Name: John Doe)
Checking out items: Laptop, Mouse, Keyboard
Paid $150.0 via PayPal (Email: [email protected])
Use Cases
- Sorting algorithms (e.g., switching between quicksort and mergesort based on data size).
- Validation rules (e.g., different password strength checks).
- Compression tools (e.g., ZIP vs. RAR compression).
Pros & Cons
| Pros | Cons |
|---|---|
| Easy to add new strategies without changing client code. | Increases the number of classes (one per strategy). |
| Clients can switch strategies at runtime. | Clients must be aware of different strategies to choose the right one. |
3. Command Pattern
Intent
Encapsulate a request as an object, thereby allowing parameterization of clients with queues, requests, and operations. Supports undoable operations.
Problem & Solution
Problem: You need to decouple the object that issues a request (invoker) from the object that performs the action (receiver). For example, a remote control (invoker) should trigger actions on devices (receivers like lights, fans) without knowing the details of how each device works.
Solution: Wrap each request in a Command object that defines an execute() method. The invoker stores and triggers commands, while the receiver performs the actual work.
Python Implementation
Let’s build a remote control with buttons that control a light and a fan.
from abc import ABC, abstractmethod
# Command Interface
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
# Concrete Commands
class LightOnCommand(Command):
def __init__(self, light: "Light"):
self._light = light
def execute(self) -> None:
self._light.turn_on()
def undo(self) -> None:
self._light.turn_off()
class LightOffCommand(Command):
def __init__(self, light: "Light"):
self._light = light
def execute(self) -> None:
self._light.turn_off()
def undo(self) -> None:
self._light.turn_on()
class FanHighCommand(Command):
def __init__(self, fan: "Fan"):
self._fan = fan
self._prev_speed = 0 # Track previous speed for undo
def execute(self) -> None:
self._prev_speed = self._fan.speed
self._fan.set_speed(3) # High speed
def undo(self) -> None:
self._fan.set_speed(self._prev_speed)
# Receiver: Devices that perform actions
class Light:
def turn_on(self) -> None:
print("Light is ON")
def turn_off(self) -> None:
print("Light is OFF")
class Fan:
def __init__(self):
self.speed = 0 # 0=off, 1=low, 2=medium, 3=high
def set_speed(self, speed: int) -> None:
self.speed = speed
print(f"Fan speed set to {speed} (0=off, 1=low, 2=medium, 3=high)")
# Invoker: Remote Control with buttons
class RemoteControl:
def __init__(self):
self._commands: dict[int, Command] = {} # Button -> Command
self._undo_stack: list[Command] = []
def set_command(self, button: int, command: Command) -> None:
self._commands[button] = command
def press_button(self, button: int) -> None:
if button in self._commands:
command = self._commands[button]
command.execute()
self._undo_stack.append(command)
def press_undo(self) -> None:
if self._undo_stack:
last_command = self._undo_stack.pop()
last_command.undo()
print("Undo performed")
# Usage
if __name__ == "__main__":
# Initialize receiver objects
living_room_light = Light()
bedroom_fan = Fan()
# Create commands
light_on = LightOnCommand(living_room_light)
light_off = LightOffCommand(living_room_light)
fan_high = FanHighCommand(bedroom_fan)
# Initialize remote control
remote = RemoteControl()
remote.set_command(0, light_on) # Button 0: Light ON
remote.set_command(1, light_off) # Button 1: Light OFF
remote.set_command(2, fan_high) # Button 2: Fan HIGH
# Press buttons
remote.press_button(0) # Light ON
remote.press_button(2) # Fan HIGH
remote.press_undo() # Undo Fan HIGH (sets to previous speed 0)
remote.press_button(1) # Light OFF
Output:
Light is ON
Fan speed set to 3 (0=off, 1=low, 2=medium, 3=high)
Fan speed set to 0 (0=off, 1=low, 2=medium, 3=high)
Undo performed
Light is OFF
Use Cases
- GUI buttons, menus, and keyboard shortcuts (each triggers a command).
- Undo/redo functionality (storing a history of commands).
- Task scheduling (queuing commands to execute later).
Pros & Cons
| Pros | Cons |
|---|---|
| Decouples invoker and receiver. | Increases code complexity (many small command classes). |
| Supports undo/redo via command history. | Simple commands may feel over-engineered. |
4. Template Method Pattern
Intent
Define the skeleton of an algorithm in a base class, deferring some steps to subclasses. Subclasses can redefine certain steps without changing the algorithm’s structure.
Problem & Solution
Problem: Multiple algorithms share the same overall structure but differ in specific steps. For example, making coffee and tea both involve boiling water, brewing, pouring into a cup, and adding condiments—but the brewing and condiment steps differ.
Solution: Define a template_method in a base class that outlines the algorithm’s steps. Mark variable steps as abstract, forcing subclasses to implement them.
Python Implementation
Let’s implement a beverage maker with coffee and tea recipes.
from abc import ABC, abstractmethod
# Abstract Base Class (Template)
class CaffeineBeverage(ABC):
# Template Method: Defines the algorithm skeleton
def prepare_recipe(self) -> None:
self.boil_water()
self.brew() # Abstract: Subclasses implement
self.pour_in_cup()
self.add_condiments() # Abstract: Subclasses implement
def boil_water(self) -> None:
print("Boiling water")
def pour_in_cup(self) -> None:
print("Pouring into cup")
@abstractmethod
def brew(self) -> None:
pass
@abstractmethod
def add_condiments(self) -> None:
pass
# Concrete Subclasses
class Coffee(CaffeineBeverage):
def brew(self) -> None:
print("Dripping coffee through filter")
def add_condiments(self) -> None:
print("Adding sugar and milk")
class Tea(CaffeineBeverage):
def brew(self) -> None:
print("Steeping the tea")
def add_condiments(self) -> None:
print("Adding lemon")
# Usage
if __name__ == "__main__":
print("Making Coffee:")
coffee = Coffee()
coffee.prepare_recipe()
print("\nMaking Tea:")
tea = Tea()
tea.prepare_recipe()
Output:
Making Coffee:
Boiling water
Dripping coffee through filter
Pouring into cup
Adding sugar and milk
Making Tea:
Boiling water
Steeping the tea
Pouring into cup
Adding lemon
Use Cases
- Frameworks (e.g., Django’s class-based views, where
dispatch()is a template method). - Report generation (common steps like fetching data, formatting, exporting; varying steps like data source).
- Game development (character creation with shared steps but unique abilities).
Pros & Cons
| Pros | Cons |
|---|---|
| Enforces a consistent algorithm structure. | Subclasses can only override specific steps (may limit flexibility). |
| Reduces code duplication (shared steps in base class). | Base class may become a “god class” if too many steps are added. |
5. Iterator Pattern
Intent
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation (e.g., list, tree, or hash table).
Problem & Solution
Problem: You need to traverse a collection (e.g., a custom BookShelf) but don’t want to expose its internal structure (e.g., a list or array). Clients should iterate over elements uniformly regardless of the collection type.
Solution: Define an Iterator interface with has_next() and next() methods. The aggregate object provides an iterator that traverses its elements, hiding the internal structure.
Python Implementation
Python has built-in iterator support via the __iter__() and __next__() methods, but we’ll implement a custom iterator for clarity.
from abc import ABC, abstractmethod
from typing import Any, Optional
# Iterator Interface
class Iterator(ABC):
@abstractmethod
def has_next(self) -> bool:
pass
@abstractmethod
def next(self) -> Any:
pass
# Aggregate Interface
class Aggregate(ABC):
@abstractmethod
def create_iterator(self) -> Iterator:
pass
# Concrete Iterator
class BookShelfIterator(Iterator):
def __init__(self, book_shelf: "BookShelf"):
self._book_shelf = book_shelf
self._index = 0
def has_next(self) -> bool:
return self._index < len(self._book_shelf.books)
def next(self) -> "Book":
if self.has_next():
book = self._book_shelf.books[self._index]
self._index += 1
return book
raise StopIteration("No more books")
# Concrete Aggregate: BookShelf
class BookShelf(Aggregate):
def __init__(self):
self.books: list["Book"] = []
def add_book(self, book: "Book") -> None:
self.books.append(book)
def create_iterator(self) -> Iterator:
return BookShelfIterator(self)
# Book Class (Element)
class Book:
def __init__(self, title: str):
self.title = title
def __str__(self) -> str:
return self.title
# Usage
if __name__ == "__main__":
# Create a BookShelf and add books
book_shelf = BookShelf()
book_shelf.add_book(Book("The Great Gatsby"))
book_shelf.add_book(Book("1984"))
book_shelf.add_book(Book("To Kill a Mockingbird"))
# Get iterator and traverse
iterator = book_shelf.create_iterator()
print("Books in shelf:")
while iterator.has_next():
book = iterator.next()
print(f"- {book}")
Output:
Books in shelf:
- The Great Gatsby
- 1984
- To Kill a Mockingbird
In Python, you’d typically use the built-in iterator protocol (e.g., for book in book_shelf), but this example illustrates the pattern’s core logic.
Use Cases
- Collections (e.g., lists, dictionaries, and custom data structures like trees or graphs).
- Streaming data (e.g., iterating over a file line by line without loading it all into memory).
- Database result sets (iterating over query results).
Pros & Cons
| Pros | Cons |
|---|---|
| Clients access elements without knowing the collection’s internal structure. | Adding new aggregate types requires new iterator types. |
| Supports multiple traversals of the same collection (via separate iterators). | Simple collections may not need a custom iterator (Python’s built-ins suffice). |
6. State Pattern
Intent
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
Problem & Solution
Problem: An object’s behavior depends on its state, and it must change behavior dynamically based on that state. For example, a vending machine behaves differently when idle, when a coin is inserted, or when dispensing a product. Using conditional statements (e.g., if-elif-else) to handle states leads to messy, unmaintainable code.
Solution: Encapsulate each state in a separate class that implements a common interface. The context object delegates state-specific behavior to the current state object, which can transition the context to another state.
Python Implementation
Let’s model a vending machine with states: IdleState, HasCoinState, and DispensingState.
from abc import ABC, abstractmethod
# State Interface
class State(ABC):
@abstractmethod
def insert_coin(self) -> None:
pass
@abstractmethod
def eject_coin(self) -> None:
pass
@abstractmethod
def press_button(self) -> None:
pass
@abstractmethod
def dispense(self) -> None:
pass
# Concrete States
class IdleState(State):
def __init__(self, vending_machine: "VendingMachine"):
self.vending_machine = vending_machine
def insert_coin(self) -> None:
print("Coin inserted. Machine is now ready to dispense.")
self.vending_machine.set_state(self.vending_machine.has_coin_state)
def eject_coin(self) -> None:
print("No coin inserted to eject.")
def press_button(self) -> None:
print("Insert a coin first.")
def dispense(self) -> None:
print("Insert a coin and press a button to dispense.")
class HasCoinState(State):
def __init__(self, vending_machine: "VendingMachine"):
self.vending_machine = vending_machine
def insert_coin(self) -> None:
print("Already has a coin. Cannot insert another.")
def eject_coin(self) -> None:
print("Coin ejected.")
self.vending_machine.set_state(self.vending_machine.idle_state)
def press_button(self) -> None:
print("Button pressed. Dispensing product...")
self.vending_machine.set_state(self.vending_machine.dispensing_state)
self.vending_machine.dispense() # Trigger dispense
def dispense(self) -> None:
print("Cannot dispense yet. Press the button.")
class DispensingState(State):
def __init__(self, vending_machine: "VendingMachine"):
self.vending_machine = vending_machine
def insert_coin(self) -> None:
print("Please wait, dispensing in progress.")
def eject_coin(self) -> None:
print("Cannot eject coin during dispensing.")
def press_button(self) -> None:
print("Already dispensing.")
def dispense(self) -> None:
self.vending_machine.release_product()
if self.vending_machine.count > 0:
self.vending_machine.set_state(self.vending_machine.idle_state)
else:
print("Machine is out of stock!")
self.vending_machine.set_state(self.vending_machine.idle_state) # Or OutOfStockState
# Context: Vending Machine
class VendingMachine:
def __init__(self, count: int = 5):
# Initialize states
self.idle_state = IdleState(self)
self.has_coin_state = HasCoinState(self)
self.dispensing_state = DispensingState(self)
self.count = count # Number of products
self.current_state: State = self.idle_state # Initial state
def set_state(self, state: State) -> None:
self.current_state = state
def release_product(self) -> None:
if self.count > 0:
print("Product dispensed. Enjoy!")
self.count -= 1
else:
print("No products left!")
# Delegate state-specific methods to current state
def insert_coin(self) -> None:
self.current_state.insert_coin()
def eject_coin(self) -> None:
self.current_state.eject_coin()
def press_button(self) -> None:
self.current_state.press_button()
def dispense(self) -> None:
self.current_state.dispense()
# Usage
if __name__ == "__main__":
vending_machine = VendingMachine(count=2) # 2 products
print("=== Test 1: Insert coin, press button, dispense ===")
vending_machine.insert_coin() # Idle -> HasCoin
vending_machine.press_button() # HasCoin -> Dispensing (triggers dispense)
print("\n=== Test 2: Insert coin, eject coin ===")
vending_machine.insert_coin() # Idle -> HasCoin
vending_machine.eject_coin() # HasCoin -> Idle
print("\n=== Test 3: Dispense last product ===")
vending_machine.insert_coin() # Idle -> HasCoin
vending_machine.press_button() # HasCoin -> Dispensing (triggers dispense)
Output:
=== Test 1: Insert coin, press button, dispense ===
Coin inserted. Machine is now ready to dispense.
Button pressed. Dispensing product...
Product dispensed. Enjoy!
=== Test 2: Insert coin, eject coin ===
Coin inserted. Machine is now ready to dispense.
Coin ejected.
=== Test 3: Dispense last product ===
Coin inserted. Machine is now ready to dispense.
Button pressed. Dispensing product...
Product dispensed. Enjoy!
Use Cases
- Finite state machines (e.g., traffic lights, elevators).
- Game characters (e.g., idle, running, attacking states).
- Order processing systems (e.g., pending, shipped, delivered states).
Pros & Cons
| Pros | Cons |
|---|---|
| Eliminates complex conditional logic. | Increases the number of classes (one per state). |
| States can be added/modified independently. | Transitions between states can become complex to track. |
Conclusion
Behavioral design patterns are indispensable for building flexible, maintainable, and scalable software. By focusing on object interaction and responsibility distribution, they solve common problems like loose coupling, dynamic behavior changes, and algorithm interchangeability.
In Python, these patterns leverage the language’s features (e.g., classes, inheritance, duck typing) to create elegant solutions. Whether you’re implementing event-driven systems with the Observer pattern, adding undo functionality with Command, or managing state transitions with State, behavioral patterns empower you to write code that’s easier to understand and extend.
Remember: Design patterns are tools, not rules. Apply them when they solve a specific problem, and avoid over-engineering simple scenarios.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Python Design Patterns - Behavioral Patterns (Refactoring.Guru)
- Real Python - Design Patterns in Python
- Python Official Documentation - Iterators