Table of Contents
Creational Patterns
Creational patterns focus on object instantiation, providing flexible ways to create objects while hiding the complexity of their creation logic.
1. Singleton
Intent
Ensure a class has only one instance and provide a global point of access to it.
Problem
Many applications require a single point of control for resources like configuration settings, database connections, or logging. For example, a configuration manager should not have multiple instances, as this could lead to inconsistent settings.
Solution
The Singleton pattern restricts instantiation to one object and exposes a static method to access it. In Python, this can be implemented using metaclasses (to control class creation) or decorators (to cache instances).
Python Implementation
Approach 1: Metaclass (Most Robust)
Metaclasses in Python define the behavior of classes. By overriding the __call__ method, we can ensure only one instance is created:
class SingletonMeta(type):
"""Metaclass to enforce Singleton behavior."""
_instances: dict[type, object] = {} # Cache for instances
def __call__(cls, *args, **kwargs):
"""Control instance creation."""
if cls not in cls._instances:
# Create a new instance if none exists
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class ConfigurationManager(metaclass=SingletonMeta):
"""Example Singleton class for managing app configurations."""
def __init__(self):
self.settings = {} # Initialize with empty settings
def set_config(self, key: str, value: str) -> None:
self.settings[key] = value
def get_config(self, key: str) -> str:
return self.settings.get(key, "Not found")
# Usage
config1 = ConfigurationManager()
config1.set_config("theme", "dark")
config2 = ConfigurationManager()
print(config2.get_config("theme")) # Output: "dark" (same instance as config1)
assert config1 is config2 # True (both reference the same object)
Approach 2: Decorator (Simpler)
A decorator can wrap a class to cache its instance. This is lighter than a metaclass but less flexible for subclassing:
from functools import wraps
def singleton(cls):
"""Decorator to enforce Singleton behavior."""
instances = {}
@wraps(cls)
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class Logger:
"""Singleton logger class."""
def log(self, message: str) -> None:
print(f"[LOG] {message}")
logger1 = Logger()
logger2 = Logger()
assert logger1 is logger2 # True
Key Takeaways
- Use Cases: Centralized resource management (e.g., database connections, configs).
- Python Quirk: Modules are singletons by default! For simple cases, a module with functions (e.g.,
config.py) may suffice instead of a class-based Singleton. - Caveats: Singletons can hinder testing (due to global state) and violate the Single Responsibility Principle. Use sparingly.
2. Factory Method
Intent
Define an interface for creating objects but let subclasses decide which class to instantiate.
Problem
You want to delegate object creation to subclasses to avoid tight coupling between a creator class and its products. For example, a document editor might support multiple document types (text, spreadsheet, presentation), but the editor shouldn’t hardcode logic for creating each type.
Solution
The Factory Method pattern defines a “factory” method in a base class that subclasses override to produce specific objects. This decouples the creator from the product.
Python Implementation
Let’s build a document editor where subclasses handle creating specific document types:
from abc import ABC, abstractmethod
# -------------------
# Product Interface
# -------------------
class Document(ABC):
"""Abstract base class for all documents."""
@abstractmethod
def open(self) -> None:
pass
@abstractmethod
def save(self) -> None:
pass
# -------------------
# Concrete Products
# -------------------
class TextDocument(Document):
def open(self) -> None:
print("Opening text document...")
def save(self) -> None:
print("Saving text document...")
class SpreadsheetDocument(Document):
def open(self) -> None:
print("Opening spreadsheet document...")
def save(self) -> None:
print("Saving spreadsheet document...")
# -------------------
# Creator Interface
# -------------------
class DocumentEditor(ABC):
"""Abstract base class for document editors."""
@abstractmethod
def create_document(self) -> Document:
"""Factory Method: Subclasses implement this to create documents."""
pass
def new_document(self) -> None:
"""Template method using the factory method."""
document = self.create_document()
document.open()
# -------------------
# Concrete Creators
# -------------------
class TextEditor(DocumentEditor):
def create_document(self) -> Document:
return TextDocument()
class SpreadsheetEditor(DocumentEditor):
def create_document(self) -> Document:
return SpreadsheetDocument()
# Usage
text_editor = TextEditor()
text_editor.new_document() # Output: "Opening text document..."
spreadsheet_editor = SpreadsheetEditor()
spreadsheet_editor.new_document() # Output: "Opening spreadsheet document..."
Key Takeaways
- Use Cases: When a class can’t anticipate the type of objects it needs to create (e.g., plugin systems).
- Python Benefit: Dynamic typing simplifies the pattern—no need for strict interfaces; duck typing works if products share methods.
- Flexibility: Adding new products (e.g.,
PresentationDocument) only requires a newPresentationEditorsubclass.
3. Builder
Intent
Separate the construction of a complex object from its representation, allowing the same construction process to create different representations.
Problem
Creating complex objects with many optional components (e.g., a computer with CPU, RAM, storage, and peripherals) can lead to bloated constructors or tangled initialization logic.
Solution
The Builder pattern splits object construction into steps handled by a “builder” object. A “director” controls the construction process, and clients use the director with a specific builder to get the desired object.
Python Implementation
Let’s build a system to construct custom computers:
from abc import ABC, abstractmethod
# -------------------
# Product
# -------------------
class Computer:
"""Complex object to be built."""
def __init__(self):
self.cpu: str = ""
self.ram: str = ""
self.storage: str = ""
self.peripherals: list[str] = []
def __str__(self) -> str:
return (f"Computer Specs:\n"
f"CPU: {self.cpu}\n"
f"RAM: {self.ram}\n"
f"Storage: {self.storage}\n"
f"Peripherals: {', '.join(self.peripherals)}")
# -------------------
# Builder Interface
# -------------------
class ComputerBuilder(ABC):
"""Abstract builder defining construction steps."""
@abstractmethod
def set_cpu(self) -> None:
pass
@abstractmethod
def set_ram(self) -> None:
pass
@abstractmethod
def set_storage(self) -> None:
pass
@abstractmethod
def add_peripherals(self) -> None:
pass
@abstractmethod
def get_computer(self) -> Computer:
pass
# -------------------
# Concrete Builders
# -------------------
class GamingPCBuilder(ComputerBuilder):
def __init__(self):
self.computer = Computer()
def set_cpu(self) -> None:
self.computer.cpu = "Intel i9-13900K"
def set_ram(self) -> None:
self.computer.ram = "32GB DDR5"
def set_storage(self) -> None:
self.computer.storage = "2TB NVMe SSD"
def add_peripherals(self) -> None:
self.computer.peripherals = ["Mechanical Keyboard", "RGB Mouse", "4K Monitor"]
def get_computer(self) -> Computer:
return self.computer
class OfficePCBuilder(ComputerBuilder):
def __init__(self):
self.computer = Computer()
def set_cpu(self) -> None:
self.computer.cpu = "Intel i5-13400"
def set_ram(self) -> None:
self.computer.ram = "16GB DDR4"
def set_storage(self) -> None:
self.computer.storage = "1TB SSD"
def add_peripherals(self) -> None:
self.computer.peripherals = ["Wireless Mouse", "HD Monitor"]
def get_computer(self) -> Computer:
return self.computer
# -------------------
# Director
# -------------------
class Director:
"""Controls the construction process."""
def __init__(self, builder: ComputerBuilder):
self.builder = builder
def build_computer(self) -> None:
self.builder.set_cpu()
self.builder.set_ram()
self.builder.set_storage()
self.builder.add_peripherals()
# Usage
# Build a gaming PC
gaming_builder = GamingPCBuilder()
director = Director(gaming_builder)
director.build_computer()
gaming_pc = gaming_builder.get_computer()
print("Gaming PC:\n", gaming_pc)
# Build an office PC
office_builder = OfficePCBuilder()
director.builder = office_builder # Reuse director with a different builder
director.build_computer()
office_pc = office_builder.get_computer()
print("\nOffice PC:\n", office_pc)
Key Takeaways
- Use Cases: Complex objects with many optional parts (e.g., meal kits, car configurations).
- Python Flexibility: Omit the Director if clients need full control over the build process.
- Benefits: Avoids telescoping constructors (e.g.,
Computer(cpu, ram, storage, peripherals, ...)).
Structural Patterns
Structural patterns organize classes and objects to form larger, flexible structures while keeping them decoupled.
1. Adapter
Intent
Convert the interface of a class into another interface clients expect, enabling incompatible classes to work together.
Problem
You have legacy code or third-party libraries with interfaces that don’t match your application’s requirements. For example, a legacy Rectangle class uses get_width() and get_height(), but your new code expects get_dimensions() to return a tuple (width, height).
Solution
The Adapter wraps the legacy class and exposes the desired interface.
Python Implementation
# -------------------
# Legacy Class (Adaptee)
# -------------------
class LegacyRectangle:
"""Legacy class with an incompatible interface."""
def __init__(self, width: float, height: float):
self._width = width
self._height = height
def get_width(self) -> float:
return self._width
def get_height(self) -> float:
return self._height
# -------------------
# Target Interface
# -------------------
class Shape(ABC):
"""Interface expected by the client."""
@abstractmethod
def get_dimensions(self) -> tuple[float, float]:
pass
# -------------------
# Adapter
# -------------------
class RectangleAdapter(Shape):
"""Adapts LegacyRectangle to the Shape interface."""
def __init__(self, legacy_rect: LegacyRectangle):
self.legacy_rect = legacy_rect # Wrap the legacy object
def get_dimensions(self) -> tuple[float, float]:
# Convert legacy interface to target interface
return (self.legacy_rect.get_width(), self.legacy_rect.get_height())
# Usage
# Client code expects Shape objects
def print_shape_dimensions(shape: Shape) -> None:
width, height = shape.get_dimensions()
print(f"Dimensions: Width={width}, Height={height}")
# Use the legacy class via the adapter
legacy_rect = LegacyRectangle(10.0, 20.0)
adapter = RectangleAdapter(legacy_rect)
print_shape_dimensions(adapter) # Output: "Dimensions: Width=10.0, Height=20.0"
Key Takeaways
- Use Cases: Integrating legacy code, third-party libraries, or refactoring without breaking existing clients.
- Python Trick: Use
__getattr__to dynamically forward calls if the interface mismatch is small (e.g.,def __getattr__(self, name): return getattr(self.legacy_rect, name)). - Types: Class Adapter (uses inheritance) vs. Object Adapter (uses composition, shown here). Composition is preferred for flexibility.
2. Decorator
Intent
Attach additional responsibilities to an object dynamically without subclassing.
Problem
You want to add features to objects (e.g., logging, caching, or validation) without creating a proliferation of subclasses (e.g., LoggedTextDocument, EncryptedTextDocument, etc.).
Solution
The Decorator pattern wraps objects in “decorator” classes that add behavior while maintaining the original interface.
Python Implementation
Let’s add condiments to a coffee order dynamically:
from abc import ABC, abstractmethod
# -------------------
# Component Interface
# -------------------
class Coffee(ABC):
"""Abstract base class for all coffees."""
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# -------------------
# Concrete Component
# -------------------
class Espresso(Coffee):
def cost(self) -> float:
return 2.50
def description(self) -> str:
return "Espresso"
# -------------------
# Decorator
# -------------------
class CoffeeDecorator(Coffee):
"""Base decorator class (wraps a Coffee)."""
def __init__(self, coffee: Coffee):
self._coffee = coffee # Wrap the coffee
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# -------------------
# Concrete Decorators
# -------------------
class Milk(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.50
def description(self) -> str:
return f"{self._coffee.description()}, Milk"
class Sugar(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.25
def description(self) -> str:
return f"{self._coffee.description()}, Sugar"
class WhippedCream(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.75
def description(self) -> str:
return f"{self._coffee.description()}, Whipped Cream"
# Usage
# Start with a base coffee
coffee = Espresso()
print(f"Basic: {coffee.description()}, Cost: ${coffee.cost():.2f}") # Espresso, $2.50
# Add milk and sugar
coffee = Milk(Sugar(coffee))
print(f"With Milk & Sugar: {coffee.description()}, Cost: ${coffee.cost():.2f}") # Espresso, Milk, Sugar, $3.25
# Add whipped cream
coffee = WhippedCream(coffee)
print(f"Fancy: {coffee.description()}, Cost: ${coffee.cost():.2f}") # Espresso, Milk, Sugar, Whipped Cream, $4.00
Key Takeaways
- Use Cases: Dynamic feature addition (e.g., logging, input validation, UI theming).
- Python Built-Ins: Python’s
functools.wrapsis a decorator for functions! Class-based decorators (shown here) work for objects. - Caveats: Overuse can lead to “decorator soup” (hard-to-follow nested wrappers).
3. Composite
Intent
Compose objects into tree structures to represent part-whole hierarchies. Clients treat individual objects and compositions uniformly.
Problem
You need to work with hierarchical data (e.g., file systems, organization charts) where elements can be either “leaves” (individual items) or “composites” (collections of items). Clients should interact with both types identically.
Solution
The Composite pattern defines a common interface for leaves and composites. Composites contain child elements (leaves or other composites) and delegate operations to their children.
Python Implementation
Let’s model a file system with files (leaves) and directories (composites):
from abc import ABC, abstractmethod
from typing import List
# -------------------
# Component Interface
# -------------------
class FileSystemComponent(ABC):
"""Abstract base class for files and directories."""
@abstractmethod
def get_name(self) -> str:
pass
@abstractmethod
def get_size(self) -> int:
pass
def add(self, component: "FileSystemComponent") -> None:
"""Optional: Add child (only implemented by composites)."""
raise NotImplementedError("add() not supported")
def remove(self, component: "FileSystemComponent") -> None:
"""Optional: Remove child (only implemented by composites)."""
raise NotImplementedError("remove() not supported")
# -------------------
# Leaf (File)
# -------------------
class File(FileSystemComponent):
def __init__(self, name: str, size: int):
self._name = name
self._size = size
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return self._size
# -------------------
# Composite (Directory)
# -------------------
class Directory(FileSystemComponent):
def __init__(self, name: str):
self._name = name
self._children: List[FileSystemComponent] = []
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
"""Sum the sizes of all children."""
return sum(child.get_size() for child in self._children)
def add(self, component: FileSystemComponent) -> None:
self._children.append(component)
def remove(self, component: FileSystemComponent) -> None:
self._children.remove(component)
def list_contents(self, indent: int = 0) -> None:
"""Recursively print directory contents."""
print(" " * indent + f"[DIR] {self._name} (Size: {self.get_size()})")
for child in self._children:
if isinstance(child, Directory):
child.list_contents(indent + 1)
else:
print(" " * (indent + 1) + f"[FILE] {child.get_name()} (Size: {child.get_size()})")
# Usage
# Build a file system tree
root = Directory("root")
docs = Directory("documents")
photos = Directory("photos")
root.add(docs)
root.add(photos)
docs.add(File("resume.pdf", 1024))
docs.add(File("notes.txt", 512))
photos.add(File("vacation.jpg", 2048))
photos.add(Directory("selfies")) # Nested directory
photos.get_children()[-1].add(File("me.jpg", 1024)) # Add file to nested directory
# Client treats root (composite) uniformly with files (leaves)
print(f"Total size of root: {root.get_size()} bytes") # 1024 + 512 + 2048 + 1024 = 4608
root.list_contents() # Recursively print the tree
Key Takeaways
- Use Cases: Hierarchical data (file systems, menus, organization charts).
- Python Benefit: Duck typing simplifies the component interface (no strict ABC required, but ABCs improve clarity).
- Key Insight: Composites delegate operations to children (e.g.,
Directory.get_size()sums child sizes).
Behavioral Patterns
Behavioral patterns focus on communication between objects, defining how they interact and distribute responsibility.
1. Observer
Intent
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Problem
You need multiple objects to react to changes in another object. For example, a weather station might notify displays (temperature, humidity, forecast) when sensor data updates.
Solution
The Observer pattern has a “Subject” (source of changes) and “Observers” (dependents). Observers register with the Subject, which notifies them of state changes.
Python Implementation
from abc import ABC, abstractmethod
from typing import List
# -------------------
# Observer Interface
# -------------------
class Observer(ABC):
"""Abstract base class for observers."""
@abstractmethod
def update(self, temperature: float, humidity: float) -> None:
pass
# -------------------
# Subject Interface
# -------------------
class Subject(ABC):
"""Abstract base class for subjects (observables)."""
@abstractmethod
def register_observer(self, observer: Observer) -> None:
pass
@abstractmethod
def remove_observer(self, observer: Observer) -> None:
pass
@abstractmethod
def notify_observers(self) -> None:
pass
# -------------------
# Concrete Subject
# -------------------
class WeatherStation(Subject):
def __init__(self):
self._observers: List[Observer] = []
self._temperature: float = 0.0
self._humidity: float = 0.0
def register_observer(self, observer: Observer) -> None:
self._observers.append(observer)
def remove_observer(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify_observers(self) -> None:
"""Notify all registered observers of state changes."""
for observer in self._observers:
observer.update(self._temperature, self._humidity)
def set_measurements(self, temperature: float, humidity: float) -> None:
"""Update state and notify observers."""
self._temperature = temperature
self._humidity = humidity
self.notify_observers()
# -------------------
# Concrete Observers
# -------------------
class CurrentConditionsDisplay(Observer):
def update(self, temperature: float, humidity: float) -> None:
print(f"Current Conditions: {temperature}°C, {humidity}% humidity")
class ForecastDisplay(Observer):
def update(self, temperature: float, humidity: float) -> None:
# Simplified forecast logic
forecast_temp = temperature + 2.0 if humidity < 60 else temperature - 1.0
print(f"Forecast: {forecast_temp}°C (based on current {temperature}°C, {humidity}% humidity)")
# Usage
weather_station = WeatherStation()
# Register observers
current_display = CurrentConditionsDisplay()
forecast_display = ForecastDisplay()
weather_station.register_observer(current_display)
weather_station.register_observer(forecast_display)
# Simulate new weather data
print("--- New Measurements: 25°C, 50% humidity ---")
weather_station.set_measurements(25.0, 50.0)
# Output:
# Current Conditions: 25.0°C, 50% humidity
# Forecast: 27.0°C (based on current 25.0°C, 50% humidity)
print("\n--- New Measurements: 30°C, 70% humidity ---")
weather_station.set_measurements(30.0, 70.0)
# Output:
# Current Conditions: 30.0°C, 70% humidity
# Forecast: 29.0°C (based on current 30.0°C, 70% humidity)
Key Takeaways
- Use Cases: Event-driven systems (e.g., UI frameworks, real-time data feeds).
- Python Built-Ins:
tornadoandPyQtuse Observer-like patterns for event handling. - Caveats: Ensure observers unregister when no longer needed to prevent memory leaks.
2. Strategy
Intent
Define a family of algorithms, encapsulate each one, and make them interchangeable.
Problem
You need to switch between different algorithms dynamically. For example, a payment processor might support credit cards, PayPal, or Bitcoin, with each requiring distinct logic.
Solution
The Strategy pattern encapsulates each algorithm in a separate “strategy” class, allowing clients to swap strategies at runtime.
Python Implementation
from abc import ABC, abstractmethod
# -------------------
# Strategy Interface
# -------------------
class PaymentStrategy(ABC):
"""Abstract base class for payment methods."""
@abstractmethod
def pay(self, amount: float) -> None:
pass
# -------------------
# Concrete Strategies
# -------------------
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str, cvv: str):
self.card_number = card_number
self.cvv = cvv
def pay(self, amount: float) -> None:
print(f"Paid ${amount:.2f} via Credit Card (****-****-****-{self.card_number[-4:]})")
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> None:
print(f"Paid ${amount:.2f} via PayPal (Account: {self.email})")
class BitcoinPayment(PaymentStrategy):
def __init__(self, wallet_address: str):
self.wallet_address = wallet_address
def pay(self, amount: float) -> None:
print(f"Paid ${amount:.2f} via Bitcoin (Wallet: {self.wallet_address[:8]}...)")
# -------------------
# Context
# -------------------
class ShoppingCart:
def __init__(self):
self._items: list[float] = []
self._payment_strategy: PaymentStrategy | None = None
def add_item(self, price: float) -> None:
self._items.append(price)
def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
"""Set the payment strategy dynamically."""
self._payment_strategy = strategy
def checkout(self) -> None:
if not self._payment_strategy:
raise ValueError("No payment strategy set!")
total = sum(self._items)
self._payment_strategy.pay(total)
# Usage
cart = ShoppingCart()
cart.add_item(49.99)
cart.add_item(29.99)
# Pay with Credit Card
cart.set_payment_strategy(CreditCardPayment("1234-5678-9012-3456", "123"))
cart.checkout() # Paid $79.98 via Credit Card (****-****-****-3456)
# Switch to PayPal
cart.set_payment_strategy(PayPalPayment("[email protected]"))
cart.checkout() # Paid $79.98 via PayPal (Account: [email protected])
Key Takeaways
- Use Cases: Dynamic algorithm selection (e.g., sorting, compression, validation).
- Python Flexibility: Strategies can be functions instead of classes (using
functools.partialfor stateful strategies). - Benefits: Eliminates conditional logic (e.g.,
if payment_type == "credit_card": ... elif ...).
3. Command
Intent
Encapsulate a request as an object, allowing for parameterization of clients with queues, requests, and operations.
Problem
You need to decouple the sender of a request from the receiver (e.g., a remote control button shouldn’t know about the device it controls). You also want to support undo/redo or queue requests.
Solution
The Command pattern wraps a request in a “command” object that exposes an execute() method. Senders (e.g., buttons) trigger commands, and receivers (e.g., devices) perform the work.
Python Implementation
Let’s build a remote control with buttons that execute commands (e.g., turn on a light, play music):
from abc import ABC, abstractmethod
# -------------------
# Command Interface
# -------------------
class Command(ABC):
"""Abstract base class for commands."""
@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 # Receiver
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 StereoOnCommand(Command):
def __init__(self, stereo: "Stereo"):
self._stereo = stereo
def execute(self) -> None:
self._stereo.turn_on()
self._stereo.set_volume(10)
def undo(self) -> None:
self._stereo.turn_off()
# -------------------
# Receivers
# -------------------
class Light:
def turn_on(self) -> None:
print("Light is ON")
def turn_off(self) -> None:
print("Light is OFF")
class Stereo:
def turn_on(self) -> None:
print("Stereo is ON")
def turn_off(self) -> None:
print("Stereo is OFF")
def set_volume(self, level: int) -> None:
print(f"Stereo volume set to {level}")
# -------------------
# Invoker
# -------------------
class RemoteControl:
def __init__(self):
self._commands: dict[int, Command] = {} # Button -> Command
self._last_command: Command | None = None # For undo
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._last_command = command
def press_undo(self) -> None:
if self._last_command:
print("Undoing last command...")
self._last_command.undo()
# Usage
# Create receivers
living_room_light = Light()
stereo = Stereo()
# Create commands
light_on = LightOnCommand(living_room_light)
light_off = LightOffCommand(living_room_light)
stereo_on = StereoOnCommand(stereo)
# Configure remote
remote = RemoteControl()
remote.set_command(0, light_on) # Button 0: Turn light on
remote.set_command(1, light_off) # Button 1: Turn light off
remote.set_command(2, stereo_on) # Button 2: Turn stereo on
# Press buttons
remote.press_button(0) # Light is ON
remote.press_button(2) # Stereo is ON; Stereo volume set to 10
remote.press_undo() # Undoing last command...; Stereo is OFF
remote.press_button(1) # Light is OFF
Key Takeaways
- Use Cases: Undo/redo, queuing requests, logging actions (e.g., text editors, remote controls).
- Python Trick: Use
__call__to make commands callable (e.g.,class Command: def __call__(self): self.execute()). - Benefits: Decouples senders (remote buttons) from receivers (lights, stereo).
Conclusion
Design patterns are not rigid rules but tools to solve common problems. Python’s flexibility—dynamic typing, first-class functions, and built-in abstractions—allows for pragmatic, often simplified implementations of GoF patterns.
By understanding the intent behind each pattern, you can apply them judiciously to write cleaner, more maintainable code. Remember: “Don’t shoehorn patterns into code”—use them only when they solve a specific problem.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Giridhar, C. (2018). Python Design Patterns. Packt Publishing.
- Refactoring Guru: GoF Design Patterns
- Real Python: Design Patterns in Python
- Python Documentation:
abcModule