Introduction
Design patterns are reusable solutions to common software design problems. Understanding these patterns helps you communicate with other developers and choose appropriate solutions. This guide covers essential patterns with modern examples.
Patterns are tools, not rules. Use them when they fit.
Creational Patterns
Singleton
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._connected = False
return cls._instance
def connect(self):
if not self._connected:
# Actual connection logic
self._connected = True
return self
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
assert db1 is db2 # Same instance
Factory Method
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process(self, amount: float) -> str:
pass
class CreditCardProcessor(PaymentProcessor):
def process(self, amount: float) -> str:
return f"Processed ${amount} via Credit Card"
class PayPalProcessor(PaymentProcessor):
def process(self, amount: float) -> str:
return f"Processed ${amount} via PayPal"
class PaymentProcessorFactory:
@staticmethod
def create(processor_type: str) -> PaymentProcessor:
processors = {
"credit_card": CreditCardProcessor,
"paypal": PayPalProcessor
}
processor_class = processors.get(processor_type)
if not processor_class:
raise ValueError(f"Unknown processor: {processor_type}")
return processor_class()
# Usage
processor = PaymentProcessorFactory.create("credit_card")
result = processor.process(100.00)
Builder
class User:
def __init__(self):
self.name = None
self.email = None
self.age = None
self.address = None
class UserBuilder:
def __init__(self):
self._user = User()
def name(self, name: str):
self._user.name = name
return self
def email(self, email: str):
self._user.email = email
return self
def age(self, age: int):
self._user.age = age
return self
def build(self) -> User:
return self._user
# Usage
user = (UserBuilder()
.name("Alice")
.email("[email protected]")
.age(30)
.build())
Structural Patterns
Adapter
class OldPaymentAPI:
def charge(self, amount: int, currency: str) -> dict:
"""Old API uses integers and different response format."""
return {"status": "ok", "amount_charged": amount}
class NewPaymentAPI:
def process_payment(self, amount: float) -> PaymentResult:
"""New API uses floats."""
return PaymentResult(success=True, amount=amount)
class PaymentAdapter:
"""Adapter to use new API with old code."""
def __init__(self, api: NewPaymentAPI):
self._api = api
def charge(self, amount: int, currency: str) -> dict:
result = self._api.process_payment(float(amount))
return {"status": "ok" if result.success else "failed"}
Decorator
def timing_decorator(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.3f}s")
return result
return wrapper
@timing_decorator
def slow_function():
import time
time.sleep(0.1)
return "done"
# Or class-based
class CacheDecorator:
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args):
if args not in self.cache:
self.cache[args] = self.func(*args)
return self.cache[args]
Behavioral Patterns
Observer
class EventObserver:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self, event):
for observer in self._observers:
observer.update(event)
class PaymentService(EventObserver):
def process_payment(self, amount):
# Process payment
self.notify({"type": "payment_processed", "amount": amount})
class NotificationObserver:
def update(self, event):
if event["type"] == "payment_processed":
print(f"Notification: Payment of ${event['amount']} processed")
Strategy
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> bool:
pass
class CreditCardStrategy(PaymentStrategy):
def __init__(self, card_number, cvv):
self.card_number = card_number
self.cvv = cvv
def pay(self, amount: float) -> bool:
# Process credit card
return True
class PayPalStrategy(PaymentStrategy):
def __init__(self, email):
self.email = email
def pay(self, amount: float) -> bool:
# Process PayPal
return True
class ShoppingCart:
def __init__(self):
self._items = []
self._payment_strategy = None
def set_payment(self, strategy: PaymentStrategy):
self._payment_strategy = strategy
def checkout(self):
total = sum(item["price"] for item in self._items)
return self._payment_strategy.pay(total)
Command
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class AddItemCommand(Command):
def __init__(self, cart, item):
self.cart = cart
self.item = item
def execute(self):
self.cart.add_item(self.item)
def undo(self):
self.cart.remove_item(self.item)
class CommandManager:
def __init__(self):
self._history = []
def execute(self, command: Command):
command.execute()
self._history.append(command)
def undo(self):
if self._history:
command = self._history.pop()
command.undo()
Best Practices
- Don’t force patterns: Use when they fit naturally
- Prefer composition over inheritance: More flexible
- Know the trade-offs: Patterns have costs
- Keep it simple: Simple solutions are better
Resources
- Design Patterns - Gang of Four - Classic GoF book
- Refactoring Guru - Design Patterns - Modern pattern explanations
- SourceMaking Design Patterns - Pattern catalog with examples
Conclusion
Design patterns provide proven solutions to common problems. Understanding these patterns helps you design better software and communicate more effectively with other developers.
Comments