Skip to main content
โšก Calmops

Design Patterns: Creational, Structural, and Behavioral Patterns

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