Table of Contents
- What Are Design Patterns?
- Creational Patterns
- Structural Patterns
- Behavioral Patterns
- When to Use Design Patterns
- Conclusion
- References
What Are Design Patterns?
Design patterns are not code snippets or libraries—they are abstract solutions to recurring problems in software design. They:
- Promote reusability: Solve problems that appear across projects.
- Enhance readability: Use a shared vocabulary (e.g., “Singleton” or “Observer”) to describe solutions.
- Improve maintainability: Decouple components, making code easier to modify.
Patterns are typically categorized into three types:
- Creational: Focus on object creation (e.g., Singleton, Factory).
- Structural: Deal with object composition (e.g., Decorator, Adapter).
- Behavioral: Manage object interactions and responsibilities (e.g., Observer, Strategy).
Creational Patterns
Creational patterns control object creation, ensuring flexibility and decoupling between object creation and usage.
Singleton
Intent
Ensure a class has only one instance and provide a global point of access to it.
Problem
You need to restrict instantiation of a class to one object (e.g., a database connection pool, logger, or configuration manager). Multiple instances could cause conflicts (e.g., duplicate database connections).
Solution
Use a class that controls its instantiation, ensuring only one instance exists. In Python, this can be implemented using the __new__ method (which creates instances) or a metaclass.
Example: Database Connection Singleton
class DatabaseConnection:
_instance = None # Class-level variable to hold the single instance
def __new__(cls):
if cls._instance is None:
print("Creating new database connection...")
cls._instance = super().__new__(cls)
# Initialize connection (e.g., to PostgreSQL)
cls._instance.connection = "psycopg2.connect(...)"
return cls._instance
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # Output: True (both are the same instance)
Use Cases
- Loggers (prevent multiple log files).
- Configuration managers (centralize app settings).
- Thread pools or connection pools.
Pros & Cons
- Pros: Single instance, controlled access, reduces resource usage.
- Cons: Global state can make testing harder; violates Single Responsibility Principle (handles instantiation and business logic).
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 the creator and the product.
Solution
Create a “factory” method in a base class that subclasses override to produce specific objects.
Example: Document Converter Factory
Suppose you need to convert files (PDF, DOCX, TXT) to HTML. Use a factory to create the appropriate converter:
from abc import ABC, abstractmethod
# Product interface: All converters must implement convert()
class Converter(ABC):
@abstractmethod
def convert(self, file_path: str) -> str:
pass
# Concrete Products
class PDFConverter(Converter):
def convert(self, file_path: str) -> str:
return f"Converted PDF at {file_path} to HTML"
class DOCXConverter(Converter):
def convert(self, file_path: str) -> str:
return f"Converted DOCX at {file_path} to HTML"
# Creator interface: Defines the factory method
class ConverterCreator(ABC):
@abstractmethod
def create_converter(self) -> Converter:
pass # Subclasses implement this
def convert_document(self, file_path: str) -> str:
converter = self.create_converter()
return converter.convert(file_path)
# Concrete Creators
class PDFConverterCreator(ConverterCreator):
def create_converter(self) -> Converter:
return PDFConverter()
class DOCXConverterCreator(ConverterCreator):
def create_converter(self) -> Converter:
return DOCXConverter()
# Usage
pdf_creator = PDFConverterCreator()
print(pdf_creator.convert_document("report.pdf")) # Output: Converted PDF at report.pdf to HTML
docx_creator = DOCXConverterCreator()
print(docx_creator.convert_document("resume.docx")) # Output: Converted DOCX at resume.docx to HTML
Use Cases
- Frameworks (e.g., web frameworks creating controllers/views based on routes).
- Plugins (dynamically loading plugins via factories).
Pros & Cons
- Pros: Decouples product creation from usage; easy to add new products.
- Cons: Requires creating a new creator subclass for each product.
Abstract Factory
Intent
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Problem
You need to create objects that belong to a “family” (e.g., UI components for Windows vs. macOS) and ensure consistency between them.
Solution
Define an abstract factory with methods for creating each family member. Concrete factories implement these methods for specific families.
Example: UI Component Factory
from abc import ABC, abstractmethod
# Abstract Products
class Button(ABC):
@abstractmethod
def render(self) -> str:
pass
class Checkbox(ABC):
@abstractmethod
def render(self) -> str:
pass
# Concrete Products (Windows)
class WindowsButton(Button):
def render(self) -> str:
return "Rendering Windows-style button"
class WindowsCheckbox(Checkbox):
def render(self) -> str:
return "Rendering Windows-style checkbox"
# Concrete Products (macOS)
class MacOSButton(Button):
def render(self) -> str:
return "Rendering macOS-style button"
class MacOSCheckbox(Checkbox):
def render(self) -> str:
return "Rendering macOS-style checkbox"
# Abstract Factory
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
# Concrete Factories
class WindowsUIFactory(UIFactory):
def create_button(self) -> Button:
return WindowsButton()
def create_checkbox(self) -> Checkbox:
return WindowsCheckbox()
class MacOSUIFactory(UIFactory):
def create_button(self) -> Button:
return MacOSButton()
def create_checkbox(self) -> Checkbox:
return MacOSCheckbox()
# Client Code
def build_ui(factory: UIFactory):
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.render())
print(checkbox.render())
# Usage
os = "windows" # Determined at runtime (e.g., via OS detection)
if os == "windows":
factory = WindowsUIFactory()
else:
factory = MacOSUIFactory()
build_ui(factory)
# Output (Windows):
# Rendering Windows-style button
# Rendering Windows-style checkbox
Use Cases
- Cross-platform applications (ensuring UI consistency).
- Theming (light/dark mode components).
Pros & Cons
- Pros: Ensures product compatibility; isolates concrete classes.
- Cons: Hard to add new product types (requires modifying all factories).
Builder
Intent
Separate the construction of a complex object from its representation, allowing the same construction process to create different representations.
Problem
You need to create an object with many optional components (e.g., a pizza with crust, cheese, toppings) without bloating the constructor.
Solution
Use a builder class to step-by-step construct the object, then retrieve the final product.
Example: Pizza Builder
class Pizza:
def __init__(self):
self.crust = None
self.cheese = None
self.toppings = []
def __str__(self):
return (f"Pizza with {self.crust} crust, {self.cheese} cheese, "
f"and toppings: {', '.join(self.toppings)}")
class PizzaBuilder:
def __init__(self):
self.pizza = Pizza()
def set_crust(self, crust: str) -> "PizzaBuilder":
self.pizza.crust = crust
return self # Enable method chaining
def set_cheese(self, cheese: str) -> "PizzaBuilder":
self.pizza.cheese = cheese
return self
def add_topping(self, topping: str) -> "PizzaBuilder":
self.pizza.toppings.append(topping)
return self
def build(self) -> Pizza:
return self.pizza
# Usage
builder = PizzaBuilder()
pizza = (builder
.set_crust("thin")
.set_cheese("mozzarella")
.add_topping("pepperoni")
.add_topping("mushrooms")
.build())
print(pizza) # Output: Pizza with thin crust, mozzarella cheese, and toppings: pepperoni, mushrooms
Use Cases
- Complex objects with many optional parts (e.g., emails, reports).
- Immutable objects (constructed step-by-step before being frozen).
Pros & Cons
- Pros: Clear, readable construction; supports different representations.
- Cons: Requires creating a separate builder for each product type.
Prototype
Intent
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
Problem
Creating new objects is expensive (e.g., database calls to fetch initial data). Instead, clone an existing “prototype” object.
Solution
Implement a clone method in the prototype class to create copies.
Example: User Profile Prototype
import copy
class UserProfile:
def __init__(self, name: str, role: str, preferences: dict):
self.name = name
self.role = role
self.preferences = preferences # Mutable attribute (needs deep copy)
def clone(self) -> "UserProfile":
# Use deepcopy to copy mutable attributes (e.g., preferences dict)
return copy.deepcopy(self)
# Create a prototype admin profile
admin_prototype = UserProfile(
name="Admin",
role="admin",
preferences={"theme": "dark", "notifications": True}
)
# Clone the prototype to create a new admin
new_admin = admin_prototype.clone()
new_admin.name = "John Doe" # Modify only the name
print(new_admin.name) # Output: John Doe
print(new_admin.role) # Output: admin (copied from prototype)
print(new_admin.preferences) # Output: {'theme': 'dark', 'notifications': True} (copied)
Use Cases
- Expensive object initialization (e.g., data-heavy objects).
- Dynamic object creation (e.g., user-defined templates).
Pros & Cons
- Pros: Avoids expensive initialization; easy to create new objects.
- Cons: Cloning complex objects with circular references can be tricky.
Structural Patterns
Structural patterns organize classes and objects to form larger structures while keeping them flexible and efficient.
Decorator
Intent
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Problem
You want to add features to individual objects (e.g., adding toppings to a coffee) without affecting other objects of the same class.
Solution
Wrap the original object in a “decorator” class that adds new behavior.
Example: Coffee with Add-ons
from abc import ABC, abstractmethod
# Component Interface
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# Concrete Component (Base Coffee)
class Espresso(Coffee):
def cost(self) -> float:
return 2.0
def description(self) -> str:
return "Espresso"
# Decorator Base Class
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# Concrete Decorators (Add-ons)
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.5
def description(self) -> str:
return f"{self._coffee.description()}, Milk"
class SugarDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.2
def description(self) -> str:
return f"{self._coffee.description()}, Sugar"
# Usage
coffee = Espresso()
print(coffee.description()) # Output: Espresso
print(coffee.cost()) # Output: 2.0
# Add milk and sugar
coffee = MilkDecorator(SugarDecorator(coffee))
print(coffee.description()) # Output: Espresso, Sugar, Milk
print(coffee.cost()) # Output: 2.7 (2.0 + 0.2 + 0.5)
Use Cases
- Adding features to objects at runtime (e.g., logging, caching).
- Python’s built-in decorators (e.g.,
@staticmethod,@lru_cache).
Pros & Cons
- Pros: Flexible, runtime extension; avoids subclass explosion.
- Cons: Can lead to complex decorator chains that are hard to debug.
Adapter
Intent
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
Problem
You need to use a legacy class with an outdated interface alongside a new system that expects a different interface.
Solution
Create an adapter class that wraps the legacy class and translates calls to the new interface.
Example: Legacy Payment Gateway Adapter
# Legacy class with outdated interface
class LegacyPaymentGateway:
def process_payment(self, amount: float, card_number: str) -> str:
return f"Legacy: Charged ${amount} to {card_number}"
# New interface expected by the system
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount: float, payment_details: dict) -> str:
pass # New interface uses a dict for payment details
# Adapter: Wraps LegacyPaymentGateway to match PaymentProcessor
class PaymentAdapter(PaymentProcessor):
def __init__(self, legacy_gateway: LegacyPaymentGateway):
self.legacy_gateway = legacy_gateway
def charge(self, amount: float, payment_details: dict) -> str:
# Translate new interface (payment_details dict) to legacy interface (card_number str)
card_number = payment_details["card_number"]
return self.legacy_gateway.process_payment(amount, card_number)
# Usage
legacy_gateway = LegacyPaymentGateway()
adapter = PaymentAdapter(legacy_gateway)
# New system uses the adapter (follows PaymentProcessor interface)
result = adapter.charge(100.0, {"card_number": "4111-1111-1111-1111"})
print(result) # Output: Legacy: Charged $100.0 to 4111-1111-1111-1111
Use Cases
- Integrating third-party libraries with incompatible interfaces.
- Legacy code modernization without rewriting.
Pros & Cons
- Pros: Reuses existing code; separates interface translation from business logic.
- Cons: Adds an extra layer of indirection, which may impact performance.
Facade
Intent
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
Problem
A complex subsystem (e.g., a video processing pipeline with encoding, filtering, and rendering) has many moving parts. Clients should not need to interact with all of them directly.
Solution
Create a facade class that hides the subsystem’s complexity and provides simple methods for common tasks.
Example: Video Processing Facade
# Complex Subsystem Components
class VideoEncoder:
def encode(self, input_file: str, format: str) -> str:
return f"Encoded {input_file} to {format}"
class VideoFilter:
def apply_filter(self, input_file: str, filter_type: str) -> str:
return f"Applied {filter_type} filter to {input_file}"
class VideoRenderer:
def render(self, input_file: str, resolution: str) -> str:
return f"Rendered {input_file} at {resolution}"
# Facade
class VideoProcessor:
def __init__(self):
self.encoder = VideoEncoder()
self.filter = VideoFilter()
self.renderer = VideoRenderer()
def process_video(self, input_file: str) -> str:
# Simplified workflow: filter → encode → render
filtered = self.filter.apply_filter(input_file, "enhance")
encoded = self.encoder.encode(filtered, "mp4")
rendered = self.renderer.render(encoded, "1080p")
return rendered
# Usage
processor = VideoProcessor()
result = processor.process_video("raw_footage.mp4")
print(result) # Output: Rendered Encoded Applied enhance filter to raw_footage.mp4 to mp4 at 1080p
Use Cases
- Simplifying client interaction with complex libraries (e.g.,
requestslibrary wrapping HTTP complexity). - Reducing dependencies between clients and subsystems.
Pros & Cons
- Pros: Simplifies usage; decouples clients from subsystem details.
- Cons: Risk of creating a “god object” that becomes a single point of failure.
Composite
Intent
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
Problem
You need to work with hierarchical data (e.g., a file system with files and folders) where individual items and groups of items should be treated the same.
Solution
Define a component interface for both leaf nodes (individual items) and composite nodes (groups). Composites contain child components and delegate operations to them.
Example: File System Composite
from abc import ABC, abstractmethod
from typing import List
# Component Interface
class FileSystemItem(ABC):
@abstractmethod
def get_size(self) -> int:
pass
@abstractmethod
def get_name(self) -> str:
pass
# Leaf Node (File)
class File(FileSystemItem):
def __init__(self, name: str, size: int):
self.name = name
self.size = size
def get_size(self) -> int:
return self.size
def get_name(self) -> str:
return self.name
# Composite Node (Folder)
class Folder(FileSystemItem):
def __init__(self, name: str):
self.name = name
self.children: List[FileSystemItem] = []
def add_child(self, item: FileSystemItem) -> None:
self.children.append(item)
def remove_child(self, item: FileSystemItem) -> None:
self.children.remove(item)
def get_size(self) -> int:
# Sum sizes of all children (recursively)
return sum(child.get_size() for child in self.children)
def get_name(self) -> str:
return self.name
# Usage
# Create files
file1 = File("document.txt", 1024)
file2 = File("image.jpg", 2048)
# Create a folder and add files
docs_folder = Folder("Documents")
docs_folder.add_child(file1)
docs_folder.add_child(file2)
# Create a root folder containing the docs folder
root_folder = Folder("Root")
root_folder.add_child(docs_folder)
# Get total size of root folder (includes all children)
print(root_folder.get_size()) # Output: 3072 (1024 + 2048)
Use Cases
- Tree structures (file systems, organization charts, XML/JSON parsers).
- UI components (e.g., buttons, panels, windows treated uniformly).
Pros & Cons
- Pros: Treats individual and composite objects uniformly; easy to add new components.
- Cons: Can make it harder to restrict components (e.g., folders can’t contain other folders).
Behavioral Patterns
Behavioral patterns focus on how objects interact and distribute responsibility.
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 to notify multiple objects (e.g., UI widgets) when a central object (e.g., a data model) changes.
Solution
Subject (observable) maintains a list of observers and notifies them of state changes. Observers register/unregister with the subject and update when notified.
Example: Weather Station
from abc import ABC, abstractmethod
from typing import List
# 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 notify observers
def set_measurements(self, temperature: float, humidity: float) -> None:
self._temperature = temperature
self._humidity = humidity
self.notify()
# 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
print(f"Forecast: {forecast_temp}°C, {humidity - 5}% humidity")
# Usage
station = WeatherStation()
current_display = CurrentConditionsDisplay()
forecast_display = ForecastDisplay()
station.attach(current_display)
station.attach(forecast_display)
# Update weather data (triggers notifications)
station.set_measurements(22.5, 65.0)
# Output:
# Current Conditions: 22.5°C, 65.0% humidity
# Forecast: 24.5°C, 60.0% humidity
Use Cases
- Event-driven systems (GUI frameworks, chat apps).
- Data synchronization (e.g., caching layers invalidating when data changes).
Pros & Cons
- Pros: Loose coupling between subject and observers; dynamic registration.
- Cons: Observers may receive unnecessary updates; can lead to memory leaks if observers aren’t detached.
Strategy
Intent
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Problem
You need to offer multiple ways to perform a task (e.g., sorting, payment processing) and allow clients to switch algorithms dynamically.
Solution
Encapsulate each algorithm in a strategy class. The context class uses a strategy object to delegate the task.
Example: Payment Processing
from abc import ABC, abstractmethod
# Strategy Interface
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> str:
pass
# Concrete Strategies
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str):
self.card_number = card_number
def pay(self, amount: float) -> str:
return f"Paid ${amount} with credit card {self.card_number}"
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> str:
return f"Paid ${amount} via PayPal ({self.email})"
# Context
class ShoppingCart:
def __init__(self, payment_strategy: PaymentStrategy):
self.payment_strategy = payment_strategy
def checkout(self, amount: float) -> str:
return self.payment_strategy.pay(amount)
# Usage
# Pay with credit card
credit_card = CreditCardPayment("4111-1111-1111-1111")
cart = ShoppingCart(credit_card)
print(cart.checkout(99.99)) # Output: Paid $99.99 with credit card 4111-1111-1111-1111
# Switch to PayPal dynamically
paypal = PayPalPayment("[email protected]")
cart.payment_strategy = paypal
print(cart.checkout(49.99)) # Output: Paid $49.99 via PayPal ([email protected])
Use Cases
- Dynamic algorithm selection (e.g., sorting with different algorithms based on data size).
- Pluggable behaviors (e.g., discount strategies in e-commerce).
Pros & Cons
- Pros: Easy to switch algorithms; open/closed principle (add new strategies without changing context).
- Cons: Clients must be aware of different strategies to select the right one.
Command
Intent
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
Problem
You need to decouple the sender of a request (e.g., a button) from the receiver (e.g., a light). The sender shouldn’t know how the receiver executes the request.
Solution
Wrap requests in command objects that implement an execute method. The sender invokes execute on the command, which delegates to the receiver.
Example: Remote Control
from abc import ABC, abstractmethod
# Command Interface
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
# Receiver
class Light:
def turn_on(self) -> None:
print("Light is ON")
def turn_off(self) -> None:
print("Light is OFF")
# 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()
# Invoker (Remote Control)
class RemoteControl:
def __init__(self):
self._command: Command | None = None
def set_command(self, command: Command) -> None:
self._command = command
def press_button(self) -> None:
if self._command:
self._command.execute()
def press_undo(self) -> None:
if self._command:
self._command.undo()
# Usage
light = Light()
on_command = LightOnCommand(light)
off_command = LightOffCommand(light)
remote = RemoteControl()
# Press "ON" button
remote.set_command(on_command)
remote.press_button() # Output: Light is ON
# Press "UNDO"
remote.press_undo() # Output: Light is OFF
# Press "OFF" button
remote.set_command(off_command)
remote.press_button() # Output: Light is OFF
Use Cases
- Undo/redo operations (e.g., text editors).
- Event queues (e.g., print spoolers, task schedulers).
Pros & Cons
- Pros: Decouples sender and receiver; supports undo/redo and logging.
- Cons: Requires creating a new command class for each operation, increasing complexity.
Template Method
Intent
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
Problem
Multiple algorithms share a common structure but differ in specific steps (e.g., making tea vs. coffee).
Solution
Define the common steps in a base class (template method) and let subclasses override the varying steps.
Example: Beverage Maker
from abc import ABC, abstractmethod
# Abstract Class with Template Method
class BeverageMaker(ABC):
# Template Method: Defines the algorithm skeleton
def make_beverage(self) -> None:
self.boil_water()
self.brew() # Varies by beverage
self.pour_in_cup()
self.add_condiments() # Varies by beverage
# Common step
def boil_water(self) -> None:
print("Boiling water")
# Common step
def pour_in_cup(self) -> None:
print("Pouring into cup")
# Abstract steps to be implemented by subclasses
@abstractmethod
def brew(self) -> None:
pass
@abstractmethod
def add_condiments(self) -> None:
pass
# Concrete Subclasses
class TeaMaker(BeverageMaker):
def brew(self) -> None:
print("Steeping the tea")
def add_condiments(self) -> None:
print("Adding lemon")
class CoffeeMaker(BeverageMaker):
def brew(self) -> None:
print("Brewing coffee grounds")
def add_condiments(self) -> None:
print("Adding sugar and milk")
# Usage
tea = TeaMaker()
print("Making tea:")
tea.make_beverage()
# Output:
# Boiling water
# Steeping the tea
# Pouring into cup
# Adding lemon
coffee = CoffeeMaker()
print("\nMaking coffee:")
coffee.make_beverage()
# Output:
# Boiling water
# Brewing coffee grounds
# Pouring into cup
# Adding sugar and milk
Use Cases
- Frameworks (e.g., web frameworks with request/response lifecycle hooks).
- Algorithms with fixed structure but variable steps (e.g., data processing pipelines).
Pros & Cons
- Pros: Enforces algorithm structure; reduces code duplication.
- Cons: Rigid algorithm structure; subclasses can only override specific steps.
When to Use Design Patterns
Design patterns are powerful tools, but they should be applied judiciously:
- Solve recurring problems: Use patterns when you encounter a common design challenge (e.g., ensuring a single instance with Singleton).
- Avoid premature optimization: Don’t force patterns where simple code suffices. Start with a simple solution, then refactor to a pattern if needed.
- Communicate intent: Use patterns to make your code more readable to other developers familiar with the pattern vocabulary.
Conclusion
Design patterns are foundational to writing clean, maintainable, and scalable OOP code. By understanding and applying patterns like Singleton, Decorator, Observer, and Strategy, you can solve complex problems with proven solutions.
Remember, patterns are not rules—they are guidelines. The key is to recognize when a pattern fits the problem, adapt it to your needs (Python’s flexibility helps here!), and prioritize clarity over教条ism.
Happy coding, and may your designs be pattern-perfect!
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martelli, A., Ravenscroft, A., & Ascher, D. (2009). Python Cookbook. O’Reilly Media.
- Real Python. “Python Design Patterns: An Introduction.” https://realpython.com/python-design-patterns-intro/
- Python.org. “Data Structures” (for Composite pattern examples). https://docs.python.org/3/tutorial/datastructures.html