python

examples

examples.py🐍
"""
Design Patterns in Python - Examples

Design patterns are reusable solutions to common problems.
Python's dynamic nature makes many patterns simpler than in other languages.
"""

# =============================================================================
# CREATIONAL PATTERNS - How objects are created
# =============================================================================

# -----------------------------------------------------------------------------
# 1. Singleton Pattern - Ensure only one instance exists
# -----------------------------------------------------------------------------

class SingletonMeta(type):
    """Metaclass implementation of Singleton."""
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class Database(metaclass=SingletonMeta):
    """Database connection - only one instance needed."""
    
    def __init__(self):
        self.connection = "Connected to database"
        print("Database initialized")
    
    def query(self, sql: str) -> str:
        return f"Executing: {sql}"


# Pythonic Singleton - Using module-level instance
class _AppConfig:
    """Configuration singleton using module pattern."""
    
    def __init__(self):
        self.debug = False
        self.database_url = "sqlite:///app.db"
        self.secret_key = "change-me"

# Module-level singleton instance
config = _AppConfig()


# -----------------------------------------------------------------------------
# 2. Factory Pattern - Create objects without specifying exact class
# -----------------------------------------------------------------------------

from abc import ABC, abstractmethod


class Animal(ABC):
    """Abstract base class for animals."""
    
    @abstractmethod
    def speak(self) -> str:
        pass


class Dog(Animal):
    def speak(self) -> str:
        return "Woof!"


class Cat(Animal):
    def speak(self) -> str:
        return "Meow!"


class Bird(Animal):
    def speak(self) -> str:
        return "Tweet!"


class AnimalFactory:
    """Factory for creating animals."""
    
    _animals = {
        "dog": Dog,
        "cat": Cat,
        "bird": Bird
    }
    
    @classmethod
    def create(cls, animal_type: str) -> Animal:
        """Create an animal by type."""
        animal_class = cls._animals.get(animal_type.lower())
        if not animal_class:
            raise ValueError(f"Unknown animal: {animal_type}")
        return animal_class()
    
    @classmethod
    def register(cls, name: str, animal_class: type):
        """Register a new animal type."""
        cls._animals[name.lower()] = animal_class


# Simple Factory Function (more Pythonic)
def create_animal(animal_type: str) -> Animal:
    """Simple factory function."""
    factories = {
        "dog": Dog,
        "cat": Cat,
        "bird": Bird
    }
    return factories[animal_type.lower()]()


# -----------------------------------------------------------------------------
# 3. Builder Pattern - Construct complex objects step by step
# -----------------------------------------------------------------------------

class Pizza:
    """A pizza built step by step."""
    
    def __init__(self):
        self.size = "medium"
        self.cheese = True
        self.toppings: list[str] = []
        self.crust = "regular"
    
    def __str__(self):
        return (f"{self.size} pizza with {self.crust} crust, "
                f"{'cheese, ' if self.cheese else ''}"
                f"toppings: {', '.join(self.toppings) or 'none'}")


class PizzaBuilder:
    """Builder for creating pizzas."""
    
    def __init__(self):
        self.pizza = Pizza()
    
    def set_size(self, size: str) -> 'PizzaBuilder':
        self.pizza.size = size
        return self
    
    def add_cheese(self, cheese: bool = True) -> 'PizzaBuilder':
        self.pizza.cheese = cheese
        return self
    
    def add_topping(self, topping: str) -> 'PizzaBuilder':
        self.pizza.toppings.append(topping)
        return self
    
    def set_crust(self, crust: str) -> 'PizzaBuilder':
        self.pizza.crust = crust
        return self
    
    def build(self) -> Pizza:
        return self.pizza


# -----------------------------------------------------------------------------
# 4. Prototype Pattern - Clone existing objects
# -----------------------------------------------------------------------------

import copy


class Document:
    """A document that can be cloned."""
    
    def __init__(self, title: str, content: str, metadata: dict | None = None):
        self.title = title
        self.content = content
        self.metadata = metadata or {}
    
    def clone(self) -> 'Document':
        """Create a deep copy of the document."""
        return copy.deepcopy(self)
    
    def __str__(self):
        return f"Document('{self.title}')"


# =============================================================================
# STRUCTURAL PATTERNS - How objects are composed
# =============================================================================

# -----------------------------------------------------------------------------
# 5. Adapter Pattern - Make incompatible interfaces work together
# -----------------------------------------------------------------------------

class OldPaymentSystem:
    """Legacy payment system with old interface."""
    
    def process_payment_old(self, amount: float, account: str) -> bool:
        print(f"OLD: Processing ${amount} for account {account}")
        return True


class NewPaymentInterface(ABC):
    """New payment interface expected by the application."""
    
    @abstractmethod
    def pay(self, amount: float, user_id: str) -> dict:
        pass


class PaymentAdapter(NewPaymentInterface):
    """Adapter to make old system work with new interface."""
    
    def __init__(self, old_system: OldPaymentSystem):
        self.old_system = old_system
    
    def pay(self, amount: float, user_id: str) -> dict:
        # Adapt the interface
        success = self.old_system.process_payment_old(amount, f"USER-{user_id}")
        return {"success": success, "amount": amount, "user_id": user_id}


# -----------------------------------------------------------------------------
# 6. Decorator Pattern (Structural, not Python decorator)
# -----------------------------------------------------------------------------

class Coffee(ABC):
    """Abstract coffee interface."""
    
    @abstractmethod
    def cost(self) -> float:
        pass
    
    @abstractmethod
    def description(self) -> str:
        pass


class SimpleCoffee(Coffee):
    """Basic coffee."""
    
    def cost(self) -> float:
        return 2.0
    
    def description(self) -> str:
        return "Coffee"


class CoffeeDecorator(Coffee):
    """Base decorator for coffee."""
    
    def __init__(self, coffee: Coffee):
        self._coffee = coffee
    
    def cost(self) -> float:
        return self._coffee.cost()
    
    def description(self) -> str:
        return self._coffee.description()


class MilkDecorator(CoffeeDecorator):
    """Add milk to coffee."""
    
    def cost(self) -> float:
        return self._coffee.cost() + 0.5
    
    def description(self) -> str:
        return self._coffee.description() + " + milk"


class SugarDecorator(CoffeeDecorator):
    """Add sugar to coffee."""
    
    def cost(self) -> float:
        return self._coffee.cost() + 0.25
    
    def description(self) -> str:
        return self._coffee.description() + " + sugar"


# -----------------------------------------------------------------------------
# 7. Facade Pattern - Simplified interface to complex subsystem
# -----------------------------------------------------------------------------

class CPU:
    def freeze(self): print("CPU: Freezing...")
    def jump(self, address: int): print(f"CPU: Jumping to {address}")
    def execute(self): print("CPU: Executing...")


class Memory:
    def load(self, address: int, data: bytes): 
        print(f"Memory: Loading data at {address}")


class HardDrive:
    def read(self, sector: int, size: int) -> bytes:
        print(f"HardDrive: Reading sector {sector}")
        return b"boot data"


class ComputerFacade:
    """Facade that simplifies computer startup."""
    
    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.hard_drive = HardDrive()
    
    def start(self):
        """Simple interface to start the computer."""
        print("Starting computer...")
        self.cpu.freeze()
        boot_data = self.hard_drive.read(0, 1024)
        self.memory.load(0, boot_data)
        self.cpu.jump(0)
        self.cpu.execute()
        print("Computer started!")


# -----------------------------------------------------------------------------
# 8. Proxy Pattern - Control access to an object
# -----------------------------------------------------------------------------

class ExpensiveResource(ABC):
    """Interface for an expensive resource."""
    
    @abstractmethod
    def operation(self) -> str:
        pass


class RealExpensiveResource(ExpensiveResource):
    """The actual expensive resource."""
    
    def __init__(self):
        print("Creating expensive resource (slow)...")
        import time
        time.sleep(0.1)  # Simulate slow creation
        self.data = "Expensive data loaded"
    
    def operation(self) -> str:
        return self.data


class LazyProxy(ExpensiveResource):
    """Proxy that delays resource creation until needed."""
    
    def __init__(self):
        self._resource: RealExpensiveResource | None = None
    
    def operation(self) -> str:
        if self._resource is None:
            self._resource = RealExpensiveResource()
        return self._resource.operation()


# =============================================================================
# BEHAVIORAL PATTERNS - How objects communicate
# =============================================================================

# -----------------------------------------------------------------------------
# 9. Observer Pattern - Notify dependents of state changes
# -----------------------------------------------------------------------------

class Subject:
    """Subject that observers watch."""
    
    def __init__(self):
        self._observers: list[callable] = []
        self._state = None
    
    def attach(self, observer: callable):
        """Attach an observer."""
        self._observers.append(observer)
    
    def detach(self, observer: callable):
        """Detach an observer."""
        self._observers.remove(observer)
    
    def notify(self):
        """Notify all observers."""
        for observer in self._observers:
            observer(self._state)
    
    @property
    def state(self):
        return self._state
    
    @state.setter
    def state(self, value):
        self._state = value
        self.notify()


# Pythonic Observer using callbacks
class EventEmitter:
    """Simple event emitter pattern."""
    
    def __init__(self):
        self._events: dict[str, list[callable]] = {}
    
    def on(self, event: str, callback: callable):
        """Subscribe to an event."""
        if event not in self._events:
            self._events[event] = []
        self._events[event].append(callback)
    
    def emit(self, event: str, *args, **kwargs):
        """Emit an event to all subscribers."""
        for callback in self._events.get(event, []):
            callback(*args, **kwargs)


# -----------------------------------------------------------------------------
# 10. Strategy Pattern - Interchangeable algorithms
# -----------------------------------------------------------------------------

from typing import Callable


# Pythonic Strategy: Just use functions!
def sort_by_price(products: list[dict]) -> list[dict]:
    return sorted(products, key=lambda p: p['price'])


def sort_by_name(products: list[dict]) -> list[dict]:
    return sorted(products, key=lambda p: p['name'])


def sort_by_rating(products: list[dict]) -> list[dict]:
    return sorted(products, key=lambda p: p['rating'], reverse=True)


class ProductSorter:
    """Context that uses a sorting strategy."""
    
    def __init__(self, strategy: Callable[[list], list] = sort_by_price):
        self.strategy = strategy
    
    def sort(self, products: list[dict]) -> list[dict]:
        return self.strategy(products)


# -----------------------------------------------------------------------------
# 11. Command Pattern - Encapsulate actions as objects
# -----------------------------------------------------------------------------

class Command(ABC):
    """Abstract command interface."""
    
    @abstractmethod
    def execute(self):
        pass
    
    @abstractmethod
    def undo(self):
        pass


class TextEditor:
    """Receiver - the object that performs the action."""
    
    def __init__(self):
        self.text = ""
    
    def insert(self, text: str, position: int):
        self.text = self.text[:position] + text + self.text[position:]
    
    def delete(self, position: int, length: int) -> str:
        deleted = self.text[position:position + length]
        self.text = self.text[:position] + self.text[position + length:]
        return deleted


class InsertCommand(Command):
    """Command to insert text."""
    
    def __init__(self, editor: TextEditor, text: str, position: int):
        self.editor = editor
        self.text = text
        self.position = position
    
    def execute(self):
        self.editor.insert(self.text, self.position)
    
    def undo(self):
        self.editor.delete(self.position, len(self.text))


class CommandHistory:
    """Invoker that manages command history."""
    
    def __init__(self):
        self._history: list[Command] = []
    
    def execute(self, command: Command):
        command.execute()
        self._history.append(command)
    
    def undo(self):
        if self._history:
            command = self._history.pop()
            command.undo()


# -----------------------------------------------------------------------------
# 12. State Pattern - Object behavior changes with state
# -----------------------------------------------------------------------------

class OrderState(ABC):
    """Abstract state for an order."""
    
    @abstractmethod
    def proceed(self, order: 'Order'):
        pass
    
    @abstractmethod
    def cancel(self, order: 'Order'):
        pass


class PendingState(OrderState):
    def proceed(self, order: 'Order'):
        print("Order confirmed, processing payment...")
        order.state = ProcessingState()
    
    def cancel(self, order: 'Order'):
        print("Order cancelled")
        order.state = CancelledState()


class ProcessingState(OrderState):
    def proceed(self, order: 'Order'):
        print("Payment received, shipping...")
        order.state = ShippedState()
    
    def cancel(self, order: 'Order'):
        print("Cannot cancel, order is processing")


class ShippedState(OrderState):
    def proceed(self, order: 'Order'):
        print("Order delivered!")
        order.state = DeliveredState()
    
    def cancel(self, order: 'Order'):
        print("Cannot cancel, order already shipped")


class DeliveredState(OrderState):
    def proceed(self, order: 'Order'):
        print("Order already delivered")
    
    def cancel(self, order: 'Order'):
        print("Cannot cancel, order delivered")


class CancelledState(OrderState):
    def proceed(self, order: 'Order'):
        print("Order was cancelled")
    
    def cancel(self, order: 'Order'):
        print("Order already cancelled")


class Order:
    """Context - order with changing states."""
    
    def __init__(self):
        self.state: OrderState = PendingState()
    
    def proceed(self):
        self.state.proceed(self)
    
    def cancel(self):
        self.state.cancel(self)


# =============================================================================
# DEMONSTRATIONS
# =============================================================================

if __name__ == "__main__":
    print("=" * 60)
    print("SINGLETON PATTERN")
    print("=" * 60)
    db1 = Database()
    db2 = Database()
    print(f"Same instance? {db1 is db2}")  # True
    
    print("\n" + "=" * 60)
    print("FACTORY PATTERN")
    print("=" * 60)
    dog = AnimalFactory.create("dog")
    cat = AnimalFactory.create("cat")
    print(f"Dog says: {dog.speak()}")
    print(f"Cat says: {cat.speak()}")
    
    print("\n" + "=" * 60)
    print("BUILDER PATTERN")
    print("=" * 60)
    pizza = (PizzaBuilder()
             .set_size("large")
             .set_crust("thin")
             .add_topping("pepperoni")
             .add_topping("mushrooms")
             .build())
    print(pizza)
    
    print("\n" + "=" * 60)
    print("DECORATOR PATTERN")
    print("=" * 60)
    coffee = SimpleCoffee()
    coffee = MilkDecorator(coffee)
    coffee = SugarDecorator(coffee)
    print(f"{coffee.description()}: ${coffee.cost()}")
    
    print("\n" + "=" * 60)
    print("OBSERVER PATTERN")
    print("=" * 60)
    emitter = EventEmitter()
    emitter.on("user_login", lambda user: print(f"User logged in: {user}"))
    emitter.on("user_login", lambda user: print(f"Sending welcome email to {user}"))
    emitter.emit("user_login", "alice@example.com")
    
    print("\n" + "=" * 60)
    print("STRATEGY PATTERN")
    print("=" * 60)
    products = [
        {"name": "Laptop", "price": 999, "rating": 4.5},
        {"name": "Phone", "price": 599, "rating": 4.8},
        {"name": "Tablet", "price": 399, "rating": 4.2},
    ]
    sorter = ProductSorter(sort_by_rating)
    sorted_products = sorter.sort(products)
    print("Sorted by rating:", [p['name'] for p in sorted_products])
    
    print("\n" + "=" * 60)
    print("STATE PATTERN")
    print("=" * 60)
    order = Order()
    order.proceed()  # Pending -> Processing
    order.proceed()  # Processing -> Shipped
    order.cancel()   # Cannot cancel
    order.proceed()  # Shipped -> Delivered
Examples - Python Tutorial | DeepML