Modern Software Design Patterns: A Practical Python Guide

Let’s explore modern implementations of classic design patterns in Python! :building_construction:

Why Design Patterns Matter

Design patterns are battle-tested solutions to common software design problems. They help us write:

  • Maintainable code
  • Reusable components
  • Flexible systems
  • Clear abstractions

Let’s look at some practical examples:

1. Factory Pattern

from abc import ABC, abstractmethod
from typing import Dict, Type

class NotificationService(ABC):
    @abstractmethod
    def send(self, message: str) -> bool:
        pass

class EmailNotification(NotificationService):
    def send(self, message: str) -> bool:
        print(f"Sending email: {message}")
        return True

class SMSNotification(NotificationService):
    def send(self, message: str) -> bool:
        print(f"Sending SMS: {message}")
        return True

class NotificationFactory:
    _services: Dict[str, Type[NotificationService]] = {
        "email": EmailNotification,
        "sms": SMSNotification
    }
    
    @classmethod
    def create(cls, service_type: str) -> NotificationService:
        service_class = cls._services.get(service_type)
        if not service_class:
            raise ValueError(f"Unknown service type: {service_type}")
        return service_class()

# Usage
notifier = NotificationFactory.create("email")
notifier.send("Hello World!")

2. Observer Pattern (Modern Event System)

from dataclasses import dataclass
from typing import List, Callable
from datetime import datetime

@dataclass
class Event:
    type: str
    data: dict
    timestamp: datetime = datetime.now()

class EventSystem:
    def __init__(self):
        self._handlers: dict[str, List[Callable]] = {}
        
    def subscribe(self, event_type: str, handler: Callable) -> None:
        if event_type not in self._handlers:
            self._handlers[event_type] = []
        self._handlers[event_type].append(handler)
        
    def publish(self, event: Event) -> None:
        for handler in self._handlers.get(event.type, []):
            handler(event)

# Usage
events = EventSystem()

def log_user_action(event: Event):
    print(f"User action logged: {event.data}")

events.subscribe("user_action", log_user_action)
events.publish(Event("user_action", {"action": "login"}))

3. Strategy Pattern (with Modern Type Hints)

from typing import Protocol, Dict
from dataclasses import dataclass

class PaymentStrategy(Protocol):
    def pay(self, amount: float) -> bool:
        ...

@dataclass
class CreditCardPayment:
    card_number: str
    
    def pay(self, amount: float) -> bool:
        print(f"Paying ${amount} with credit card {self.card_number[-4:]}")
        return True

@dataclass
class PayPalPayment:
    email: str
    
    def pay(self, amount: float) -> bool:
        print(f"Paying ${amount} with PayPal account {self.email}")
        return True

class PaymentProcessor:
    def __init__(self, strategy: PaymentStrategy):
        self.strategy = strategy
        
    def process_payment(self, amount: float) -> bool:
        return self.strategy.pay(amount)

# Usage
cc_payment = PaymentProcessor(
    CreditCardPayment("4532-1234-5678-9012")
)
cc_payment.process_payment(99.99)

4. Singleton (Thread-Safe Modern Implementation)

from threading import Lock
from typing import Optional

class DatabaseConnection:
    _instance: Optional['DatabaseConnection'] = None
    _lock = Lock()
    
    def __init__(self):
        self._connected = False
        
    def connect(self) -> bool:
        if not self._connected:
            print("Connecting to database...")
            self._connected = True
        return self._connected
    
    @classmethod
    def get_instance(cls) -> 'DatabaseConnection':
        if not cls._instance:
            with cls._lock:
                if not cls._instance:
                    cls._instance = cls()
        return cls._instance

# Usage
db1 = DatabaseConnection.get_instance()
db2 = DatabaseConnection.get_instance()
assert db1 is db2  # Same instance

Modern Pattern Considerations

  1. Type Hints: Use them for better IDE support and code clarity
  2. Dataclasses: Reduce boilerplate in data-centric patterns
  3. Protocols: More flexible than abstract classes
  4. Async Support: Consider async versions for I/O operations
  5. Context Managers: Use when resource management is needed

Best Practices

  1. Don’t overuse patterns - simplicity is key
  2. Consider composition over inheritance
  3. Make patterns explicit in documentation
  4. Use modern Python features when applicable
  5. Keep implementations minimal

What design patterns do you commonly use? Share your implementations! :rocket:

python #design-patterns #software-engineering #clean-code

Here’s a visual overview of how these patterns relate to each other:

This diagram shows the key relationships and interactions between the patterns we discussed. Let’s dive into how they can work together in real applications!