Skip to main content
โšก Calmops

Software Design Patterns: Classic and Modern Applications

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

  1. Don’t force patterns: Use when they fit naturally
  2. Prefer composition over inheritance: More flexible
  3. Know the trade-offs: Patterns have costs
  4. Keep it simple: Simple solutions are better

Resources

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