Introduction
Design patterns are proven solutions to common software design problems. They provide vocabulary for discussing architecture and help avoid reinventing the wheel. This guide covers the most useful patterns for modern applications.
Creational Patterns
Factory Pattern
from abc import ABC, abstractmethod
from typing import Dict, Type
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount: float) -> bool:
pass
class CreditCard(PaymentMethod):
def __init__(self, card_number: str, expiry: str):
self.card_number = card_number
self.expiry = expiry
def pay(self, amount: float) -> bool:
print(f"Paid ${amount} with credit card")
return True
class PayPal(PaymentMethod):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> bool:
print(f"Paid ${amount} with PayPal")
return True
class PaymentMethodFactory:
_methods: Dict[str, Type[PaymentMethod]] = {}
@classmethod
def register(cls, name: str, method_class: Type[PaymentMethod]):
cls._methods[name] = method_class
@classmethod
def create(cls, payment_type: str, **kwargs) -> PaymentMethod:
method_class = cls._methods.get(payment_type)
if not method_class:
raise ValueError(f"Unknown payment type: {payment_type}")
return method_class(**kwargs)
# Usage
PaymentMethodFactory.register("credit_card", CreditCard)
PaymentMethodFactory.register("paypal", PayPal)
payment = PaymentMethodFactory.create("credit_card", card_number="1234", expiry="12/25")
payment.pay(99.99)
Singleton Pattern
class DatabaseConnection:
_instance = None
_connection = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def connect(self, uri: str):
if self._connection is None:
print(f"Connecting to {uri}")
self._connection = uri
return self._connection
def query(self, sql: str):
return f"Executing: {sql}"
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
assert db1 is db2 # Same instance
Structural Patterns
Adapter Pattern
class OldPaymentSystem:
def process_payment(self, card: str, amount: int) -> str:
return f"Processed ${amount} with old system using {card}"
class NewPaymentSystem:
def pay(self, amount: float, token: str) -> bool:
return True
class PaymentAdapter(NewPaymentSystem):
def __init__(self, old_system: OldPaymentSystem):
self.old_system = old_system
def pay(self, amount: float, token: str) -> bool:
# Adapt old interface to new
result = self.old_system.process_payment(token, int(amount))
return "Processed" in result
# Usage
old = OldPaymentSystem()
adapter = PaymentAdapter(old)
adapter.pay(99.99, "card123")
Behavioral Patterns
Observer Pattern
from abc import ABC, abstractmethod
from typing import List
class Observer(ABC):
@abstractmethod
def update(self, message: str):
pass
class Subject:
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self, message: str):
for observer in self._observers:
observer.update(message)
class UserNotifier(Observer):
def update(self, message: str):
print(f"Notification: {message}")
# Usage
subject = Subject()
subject.attach(UserNotifier())
subject.notify("Your order has shipped!")
Strategy Pattern
from abc import ABC, abstractmethod
from typing import List
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: List) -> List:
pass
class QuickSort(SortStrategy):
def sort(self, data: List) -> List:
return sorted(data) # Python's Timsort
class ReverseSort(SortStrategy):
def sort(self, data: List) -> List:
return sorted(data, reverse=True)
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self._strategy = strategy
def execute(self, data: List) -> List:
return self._strategy.sort(data)
# Usage
sorter = Sorter(QuickSort())
print(sorter.execute([3, 1, 4, 1, 5]))
sorter.set_strategy(ReverseSort())
print(sorter.execute([3, 1, 4, 1, 5]))
Conclusion
Design patterns provide reusable solutions to common problems. Use Factory for object creation, Adapter for interface compatibility, Observer for event systems, and Strategy for interchangeable algorithms. Apply patterns judiciouslyโdon’t force patterns where simple code suffices.
Resources
- “Design Patterns: Elements of Reusable Object-Oriented Software” by Gang of Four
- Refactoring Guru - Design Patterns
Comments