Design Patterns

Proven solutions to common software design problems

The Language of Software Design

Design patterns are reusable solutions to recurring problems in software design. They're not finished code you can copy-paste, but templates for how to solve problems in various contexts. Introduced by the "Gang of Four" (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their seminal 1994 book, these patterns represent decades of collective wisdom from experienced developers. Mastering design patterns gives you a vocabulary to communicate complex ideas succinctly and provides battle-tested approaches to common architectural challenges.

Three Categories of Patterns

🏗️ Creational

Purpose: Object creation mechanisms

🔗 Structural

Purpose: Object composition

🏗️ CREATIONAL PATTERNS

Patterns for creating objects in a manner suitable to the situation

Singleton Pattern

Intent: Ensure a class has only one instance and provide a global point of access to it. This is useful for managing shared resources like database connections, configuration managers, or logging systems.

⚠️ Warning: Singleton is often considered an anti-pattern because it introduces global state and makes testing difficult. Use sparingly and consider alternatives like dependency injection.

Basic Implementation

# Thread-safe Singleton in Python
import threading

class DatabaseConnection:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                # Double-checked locking for thread safety
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        # Ensure initialization happens only once
        if self._initialized:
            return

        self._initialized = True
        self.connection = self._create_connection()
        print("Database connection established")

    def _create_connection(self):
        # Actual connection logic
        return "db_connection_object"

    def query(self, sql):
        return f"Executing: {sql}"

# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True - same instance

# Both refer to the same connection
print(db1.query("SELECT * FROM users"))
print(db2.query("SELECT * FROM orders"))

Modern Python Approach: Metaclass

# More Pythonic using metaclass
class SingletonMeta(type):
    _instances = {}
    _lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            with cls._lock:
                if cls not in cls._instances:
                    cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Logger(metaclass=SingletonMeta):
    def __init__(self):
        self.log_file = "app.log"

    def log(self, message):
        print(f"[LOG] {message}")

# Or use decorator approach
def singleton(cls):
    instances = {}
    lock = threading.Lock()

    def get_instance(*args, **kwargs):
        if cls not in instances:
            with lock:
                if cls not in instances:
                    instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance

@singleton
class ConfigManager:
    def __init__(self):
        self.config = self._load_config()

    def _load_config(self):
        return {"api_key": "secret", "timeout": 30}

    def get(self, key):
        return self.config.get(key)

When to Use Singleton

  • Shared resource management: Database connections, file handles, thread pools
  • Configuration: Application-wide settings that should be consistent
  • Logging: Centralized logging system
  • Caching: Global cache that all parts of application use
Better Alternative: Consider using dependency injection with a DI container that manages object lifetimes instead of hardcoding Singleton behavior.
✅ Pros
  • Guaranteed single instance
  • Global access point
  • Lazy initialization possible
  • Memory efficient (one object)
❌ Cons
  • Global state - hard to test
  • Violates Single Responsibility
  • Hides dependencies
  • Threading complexity
  • Often considered anti-pattern

Factory Method Pattern

Intent: Define an interface for creating objects, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses. This promotes loose coupling by eliminating the need to bind application code to specific classes.

Problem: Direct Instantiation

# BAD: Tightly coupled to concrete classes
class Application:
    def __init__(self, transport_type):
        if transport_type == "truck":
            self.transport = Truck()
        elif transport_type == "ship":
            self.transport = Ship()
        elif transport_type == "plane":
            self.transport = Plane()
        # Adding new transport requires modifying this class!

    def deliver(self):
        self.transport.deliver()

Solution: Factory Method

from abc import ABC, abstractmethod

# Product interface
class Transport(ABC):
    @abstractmethod
    def deliver(self):
        pass

# Concrete products
class Truck(Transport):
    def deliver(self):
        return "Delivering by land in a truck"

class Ship(Transport):
    def deliver(self):
        return "Delivering by sea in a ship"

class Plane(Transport):
    def deliver(self):
        return "Delivering by air in a plane"

# Creator (Factory) interface
class LogisticsCompany(ABC):
    @abstractmethod
    def create_transport(self) -> Transport:
        """Factory method - subclasses override this"""
        pass

    def plan_delivery(self):
        # Uses factory method to get transport
        transport = self.create_transport()
        result = transport.deliver()
        return f"Logistics: {result}"

# Concrete creators
class RoadLogistics(LogisticsCompany):
    def create_transport(self) -> Transport:
        return Truck()

class SeaLogistics(LogisticsCompany):
    def create_transport(self) -> Transport:
        return Ship()

class AirLogistics(LogisticsCompany):
    def create_transport(self) -> Transport:
        return Plane()

# Usage - client code works with abstract types
def client_code(logistics: LogisticsCompany):
    print(logistics.plan_delivery())

# Easy to extend - add new logistics without modifying existing code
client_code(RoadLogistics())  # Delivering by land
client_code(SeaLogistics())   # Delivering by sea
client_code(AirLogistics())   # Delivering by air

Simple Factory (Not GoF Pattern)

# Simple Factory - useful for straightforward cases
class TransportFactory:
    @staticmethod
    def create_transport(transport_type: str) -> Transport:
        factories = {
            "truck": Truck,
            "ship": Ship,
            "plane": Plane
        }

        transport_class = factories.get(transport_type)
        if not transport_class:
            raise ValueError(f"Unknown transport type: {transport_type}")

        return transport_class()

# Usage
factory = TransportFactory()
transport = factory.create_transport("truck")
print(transport.deliver())

Real-World Example: Document Creators

# Creating different document formats
class Document(ABC):
    @abstractmethod
    def render(self) -> str:
        pass

class PDFDocument(Document):
    def render(self) -> str:
        return "<PDF Content>"

class WordDocument(Document):
    def render(self) -> str:
        return "<Word Content>"

class HTMLDocument(Document):
    def render(self) -> str:
        return "<HTML Content>"

class DocumentCreator(ABC):
    @abstractmethod
    def create_document(self) -> Document:
        pass

    def export_document(self, content: str):
        doc = self.create_document()
        rendered = doc.render()
        return f"Exporting {content} as {rendered}"

class PDFCreator(DocumentCreator):
    def create_document(self) -> Document:
        return PDFDocument()

class WordCreator(DocumentCreator):
    def create_document(self) -> Document:
        return WordDocument()

# In application
def export_report(creator: DocumentCreator, data: str):
    return creator.export_document(data)

export_report(PDFCreator(), "Q4 Sales Report")
export_report(WordCreator(), "Meeting Notes")
💡 When to Use Factory Method: Use it when you don't know beforehand the exact types and dependencies of objects your code should work with, or when you want to provide users with a way to extend your library or framework.
✅ Pros
  • Loose coupling - decouples creation from use
  • Single Responsibility Principle
  • Open/Closed Principle - easy to add new types
  • More testable code
❌ Cons
  • Can lead to many subclasses
  • More complex than direct instantiation
  • May be overkill for simple cases

Builder Pattern

Intent: Separate the construction of a complex object from its representation, allowing the same construction process to create different representations. Particularly useful when an object has many optional parameters.

Problem: Constructor Hell

# BAD: Too many constructor parameters
class Pizza:
    def __init__(
        self,
        size,
        cheese=True,
        pepperoni=False,
        mushrooms=False,
        onions=False,
        bacon=False,
        extra_cheese=False,
        green_peppers=False,
        pineapple=False,
        olives=False
    ):
        # Confusing! What does each parameter mean?
        pass

# Usage is unclear
pizza = Pizza("large", True, False, True, False, True, False, False, False, True)
# What is what?

Solution: Builder Pattern

class Pizza:
    def __init__(self):
        self.size = None
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False
        self.onions = False
        self.bacon = False

    def __str__(self):
        toppings = []
        if self.cheese: toppings.append("cheese")
        if self.pepperoni: toppings.append("pepperoni")
        if self.mushrooms: toppings.append("mushrooms")
        if self.onions: toppings.append("onions")
        if self.bacon: toppings.append("bacon")

        return f"{self.size} pizza with {', '.join(toppings)}"

class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()

    def set_size(self, size):
        self.pizza.size = size
        return self  # Return self for method chaining

    def add_cheese(self):
        self.pizza.cheese = True
        return self

    def add_pepperoni(self):
        self.pizza.pepperoni = True
        return self

    def add_mushrooms(self):
        self.pizza.mushrooms = True
        return self

    def add_onions(self):
        self.pizza.onions = True
        return self

    def add_bacon(self):
        self.pizza.bacon = True
        return self

    def build(self):
        return self.pizza

# Usage - crystal clear!
pizza = (PizzaBuilder()
    .set_size("large")
    .add_cheese()
    .add_pepperoni()
    .add_mushrooms()
    .add_bacon()
    .build())

print(pizza)  # large pizza with cheese, pepperoni, mushrooms, bacon

Advanced: Director + Builder

# Director knows how to build specific configurations
class PizzaDirector:
    def __init__(self, builder: PizzaBuilder):
        self.builder = builder

    def make_margherita(self):
        return (self.builder
            .set_size("medium")
            .add_cheese()
            .build())

    def make_meat_lovers(self):
        return (self.builder
            .set_size("large")
            .add_cheese()
            .add_pepperoni()
            .add_bacon()
            .build())

    def make_veggie(self):
        return (self.builder
            .set_size("medium")
            .add_cheese()
            .add_mushrooms()
            .add_onions()
            .build())

# Usage
director = PizzaDirector(PizzaBuilder())
margherita = director.make_margherita()
meat_lovers = director.make_meat_lovers()

print(margherita)
print(meat_lovers)

Real-World: Query Builder

class SQLQuery:
    def __init__(self):
        self.table = None
        self.columns = []
        self.conditions = []
        self.order_by = None
        self.limit_val = None

    def to_sql(self):
        columns_str = ", ".join(self.columns) if self.columns else "*"
        sql = f"SELECT {columns_str} FROM {self.table}"

        if self.conditions:
            sql += " WHERE " + " AND ".join(self.conditions)

        if self.order_by:
            sql += f" ORDER BY {self.order_by}"

        if self.limit_val:
            sql += f" LIMIT {self.limit_val}"

        return sql

class QueryBuilder:
    def __init__(self):
        self.query = SQLQuery()

    def select(self, *columns):
        self.query.columns = list(columns)
        return self

    def from_table(self, table):
        self.query.table = table
        return self

    def where(self, condition):
        self.query.conditions.append(condition)
        return self

    def order_by(self, column):
        self.query.order_by = column
        return self

    def limit(self, n):
        self.query.limit_val = n
        return self

    def build(self):
        return self.query

# Usage - fluent SQL building
query = (QueryBuilder()
    .select("name", "email", "age")
    .from_table("users")
    .where("age > 18")
    .where("status = 'active'")
    .order_by("name")
    .limit(10)
    .build())

print(query.to_sql())
# SELECT name, email, age FROM users WHERE age > 18 AND status = 'active' ORDER BY name LIMIT 10
💡 Python Alternative: For simple cases, consider using dataclasses withkw_only=True or keyword arguments, which can provide similar benefits without the complexity of the full Builder pattern.
✅ Pros
  • Readable, fluent interface
  • Step-by-step object construction
  • Different representations possible
  • Isolates complex construction code
  • Optional parameters are clear
❌ Cons
  • Increases code complexity
  • More classes to maintain
  • Overkill for simple objects
  • Mutable during construction

Abstract Factory Pattern

Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes. Think of it as a "factory of factories" - useful when you need to create multiple products that work together.

💡 Key Difference: Factory Method creates one product, Abstract Factory creates families of related products (e.g., Windows UI components vs macOS UI components).

Problem: Platform-Specific UI

# BAD: Mixing components from different platforms
app = Application()
app.button = WindowsButton()  # Windows style
app.checkbox = MacCheckbox()  # Mac style - inconsistent!

Solution: Abstract Factory

from abc import ABC, abstractmethod

# Abstract products
class Button(ABC):
    @abstractmethod
    def render(self):
        pass

class Checkbox(ABC):
    @abstractmethod
    def render(self):
        pass

# Concrete products - Windows family
class WindowsButton(Button):
    def render(self):
        return "[Windows Button]"

class WindowsCheckbox(Checkbox):
    def render(self):
        return "[Windows Checkbox]"

# Concrete products - Mac family
class MacButton(Button):
    def render(self):
        return "[Mac Button]"

class MacCheckbox(Checkbox):
    def render(self):
        return "[Mac Checkbox]"

# Abstract factory
class GUIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        pass

    @abstractmethod
    def create_checkbox(self) -> Checkbox:
        pass

# Concrete factories
class WindowsFactory(GUIFactory):
    def create_button(self) -> Button:
        return WindowsButton()

    def create_checkbox(self) -> Checkbox:
        return WindowsCheckbox()

class MacFactory(GUIFactory):
    def create_button(self) -> Button:
        return MacButton()

    def create_checkbox(self) -> Checkbox:
        return MacCheckbox()

# Client code works with abstract types
class Application:
    def __init__(self, factory: GUIFactory):
        self.factory = factory

    def create_ui(self):
        button = self.factory.create_button()
        checkbox = self.factory.create_checkbox()
        return f"{button.render()} + {checkbox.render()}"

# Usage - consistent product families
import platform

if platform.system() == "Windows":
    factory = WindowsFactory()
else:
    factory = MacFactory()

app = Application(factory)
print(app.create_ui())  # All components match the platform

Real-World: Database Access

# Creating database-specific components
class Connection(ABC):
    @abstractmethod
    def connect(self): pass

class Command(ABC):
    @abstractmethod
    def execute(self, sql): pass

class PostgreSQLConnection(Connection):
    def connect(self):
        return "PostgreSQL connection established"

class PostgreSQLCommand(Command):
    def execute(self, sql):
        return f"Executing on PostgreSQL: {sql}"

class MySQLConnection(Connection):
    def connect(self):
        return "MySQL connection established"

class MySQLCommand(Command):
    def execute(self, sql):
        return f"Executing on MySQL: {sql}"

class DatabaseFactory(ABC):
    @abstractmethod
    def create_connection(self) -> Connection:
        pass

    @abstractmethod
    def create_command(self) -> Command:
        pass

class PostgreSQLFactory(DatabaseFactory):
    def create_connection(self) -> Connection:
        return PostgreSQLConnection()

    def create_command(self) -> Command:
        return PostgreSQLCommand()

class MySQLFactory(DatabaseFactory):
    def create_connection(self) -> Connection:
        return MySQLConnection()

    def create_command(self) -> Command:
        return MySQLCommand()

# Client uses consistent database components
class DatabaseManager:
    def __init__(self, factory: DatabaseFactory):
        self.connection = factory.create_connection()
        self.command = factory.create_command()

    def query(self, sql):
        print(self.connection.connect())
        return self.command.execute(sql)

# Switch databases by changing factory
db = DatabaseManager(PostgreSQLFactory())
result = db.query("SELECT * FROM users")

When to Use Abstract Factory

  • Product families: When you need to create sets of related objects
  • Cross-platform apps: Different UI components per platform
  • Theme systems: Consistent look and feel across components
  • Database abstraction: Swap entire database implementations
✅ Pros
  • Ensures product compatibility
  • Easy to swap entire product families
  • Isolates concrete classes
❌ Cons
  • Complex - many classes involved
  • Hard to add new product types
  • Can be overkill for simple cases

Prototype Pattern

Intent: Create new objects by copying existing objects (prototypes) rather than creating from scratch. Useful when object creation is expensive or when you need to create many similar objects.

💡 Python Power: Python provides built-in cloning via the copymodule. The Prototype pattern formalizes this approach.

Basic Implementation

import copy
from abc import ABC, abstractmethod

class Prototype(ABC):
    @abstractmethod
    def clone(self):
        pass

class Shape(Prototype):
    def __init__(self, x, y, color):
        self.x = x
        self.y = y
        self.color = color

    def clone(self):
        # Shallow copy
        return copy.copy(self)

    def deep_clone(self):
        # Deep copy - for nested objects
        return copy.deepcopy(self)

class Circle(Shape):
    def __init__(self, x, y, color, radius):
        super().__init__(x, y, color)
        self.radius = radius

    def __str__(self):
        return f"Circle at ({self.x},{self.y}), color: {self.color}, radius: {self.radius}"

# Usage - clone instead of creating from scratch
original = Circle(10, 20, "red", 5)
print(original)

# Clone and modify
clone1 = original.clone()
clone1.x = 50
clone1.color = "blue"
print(clone1)

# Original unchanged
print(original)  # Still at (10, 20), red

Real-World: Document Templates

class DocumentTemplate:
    def __init__(self, title, content, styles):
        self.title = title
        self.content = content
        self.styles = styles  # Complex nested structure

    def clone(self):
        return copy.deepcopy(self)

    def __str__(self):
        return f"Document: {self.title}"

# Create expensive template once
base_template = DocumentTemplate(
    title="Report Template",
    content={"sections": ["intro", "body", "conclusion"]},
    styles={"font": "Arial", "size": 12, "colors": {"heading": "#000", "body": "#333"}}
)

# Quickly create customized documents by cloning
q1_report = base_template.clone()
q1_report.title = "Q1 Sales Report"
q1_report.content["data"] = "Q1 sales data..."

q2_report = base_template.clone()
q2_report.title = "Q2 Sales Report"
q2_report.content["data"] = "Q2 sales data..."

print(q1_report)
print(q2_report)

Advanced: Prototype Registry

class PrototypeRegistry:
    """Central repository of prototype objects"""
    def __init__(self):
        self._prototypes = {}

    def register(self, name, prototype):
        self._prototypes[name] = prototype

    def unregister(self, name):
        del self._prototypes[name]

    def clone(self, name):
        prototype = self._prototypes.get(name)
        if not prototype:
            raise ValueError(f"Prototype '{name}' not found")
        return prototype.clone()

# Game character system
class Character:
    def __init__(self, name, health, strength, equipment):
        self.name = name
        self.health = health
        self.strength = strength
        self.equipment = equipment

    def clone(self):
        return copy.deepcopy(self)

# Setup registry with common character types
registry = PrototypeRegistry()

warrior_proto = Character("Warrior", 100, 80, ["sword", "shield"])
mage_proto = Character("Mage", 60, 30, ["staff", "robe"])
archer_proto = Character("Archer", 75, 50, ["bow", "arrows"])

registry.register("warrior", warrior_proto)
registry.register("mage", mage_proto)
registry.register("archer", archer_proto)

# Quickly create customized characters
player1 = registry.clone("warrior")
player1.name = "Conan"

player2 = registry.clone("mage")
player2.name = "Gandalf"

player3 = registry.clone("archer")
player3.name = "Legolas"

When to Use Prototype

  • Expensive creation: Object initialization is costly (DB queries, file I/O)
  • Similar objects: Need many objects with slight variations
  • Runtime types: Class of object determined at runtime
  • Avoid subclasses: Reduce number of factory subclasses
✅ Pros
  • Avoid expensive initialization
  • Reduce subclassing
  • Add/remove objects at runtime
  • Configure objects dynamically
❌ Cons
  • Circular references can be tricky
  • Deep copy can be expensive
  • Need to implement clone properly

🔗 STRUCTURAL PATTERNS

Patterns for composing objects to form larger structures

Adapter Pattern

Intent: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. Think of it as a translator between two incompatible systems.

# Third-party library with incompatible interface
class LegacyPaymentGateway:
    def make_payment(self, amount_in_cents):
        print(f"Processing {amount_in_cents} cents")
        return {"status": "OK", "transaction_id": "12345"}

# Our application expects this interface
class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount_in_dollars) -> bool:
        pass

# Adapter bridges the gap
class LegacyPaymentAdapter(PaymentProcessor):
    def __init__(self, legacy_gateway: LegacyPaymentGateway):
        self.legacy_gateway = legacy_gateway

    def process_payment(self, amount_in_dollars) -> bool:
        # Convert dollars to cents
        amount_in_cents = int(amount_in_dollars * 100)

        # Call legacy method
        result = self.legacy_gateway.make_payment(amount_in_cents)

        # Adapt response format
        return result.get("status") == "OK"

# Usage - client works with our interface
legacy_system = LegacyPaymentGateway()
adapter = LegacyPaymentAdapter(legacy_system)

# Our code doesn't know it's using a legacy system
if adapter.process_payment(99.99):
    print("Payment successful!")

# Real example: Different API formats
class XMLDataProvider:
    def get_xml_data(self):
        return "<users><user><name>Alice</name></user></users>"

class JSONDataConsumer:
    def process_data(self, json_data):
        # Expects JSON format
        print(f"Processing: {json_data}")

class XMLToJSONAdapter:
    def __init__(self, xml_provider: XMLDataProvider):
        self.xml_provider = xml_provider

    def get_json_data(self):
        import xml.etree.ElementTree as ET
        import json

        xml_data = self.xml_provider.get_xml_data()
        root = ET.fromstring(xml_data)

        # Convert XML to JSON
        users = []
        for user in root.findall('user'):
            users.append({"name": user.find('name').text})

        return json.dumps({"users": users})

xml_source = XMLDataProvider()
adapter = XMLToJSONAdapter(xml_source)
consumer = JSONDataConsumer()
consumer.process_data(adapter.get_json_data())
✅ Pros
  • Makes incompatible interfaces work together
  • Single Responsibility Principle
  • Open/Closed Principle
  • Wraps legacy code cleanly
❌ Cons
  • Increases overall code complexity
  • Additional layer of indirection
  • Sometimes simpler to change the interface

Decorator Pattern

Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. You can stack multiple decorators to compose complex behaviors.

# Component interface
class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass

    @abstractmethod
    def description(self) -> str:
        pass

# Concrete component
class SimpleCoffee(Coffee):
    def cost(self) -> float:
        return 2.0

    def description(self) -> str:
        return "Simple coffee"

# Base decorator
class CoffeeDecorator(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()

# Concrete decorators
class Milk(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.5

    def description(self) -> str:
        return self._coffee.description() + ", milk"

class Sugar(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.2

    def description(self) -> str:
        return self._coffee.description() + ", sugar"

class WhippedCream(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.7

    def description(self) -> str:
        return self._coffee.description() + ", whipped cream"

# Usage - wrap decorators dynamically
coffee = SimpleCoffee()
print(f"{coffee.description()}: {coffee.cost()}")

# Add milk
coffee = Milk(coffee)
print(f"{coffee.description()}: {coffee.cost()}")

# Add sugar
coffee = Sugar(coffee)
print(f"{coffee.description()}: {coffee.cost()}")

# Add whipped cream
coffee = WhippedCream(coffee)
print(f"{coffee.description()}: {coffee.cost()}")

# Can stack in any order!
fancy_coffee = WhippedCream(Sugar(Milk(SimpleCoffee())))
print(f"{fancy_coffee.description()}: {fancy_coffee.cost()}")

Real-World: Logging Decorator

# Decorating functions with additional behavior
import time
import functools

def timer(func):
    """Decorator to measure execution time"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.2f} seconds")
        return result
    return wrapper

def log_calls(func):
    """Decorator to log function calls"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

def cache(func):
    """Decorator to cache results"""
    cached_results = {}

    @functools.wraps(func)
    def wrapper(*args):
        if args not in cached_results:
            cached_results[args] = func(*args)
        return cached_results[args]
    return wrapper

# Stack multiple decorators
@timer
@log_calls
@cache
def expensive_computation(n):
    time.sleep(1)  # Simulate expensive operation
    return n * n

# First call - slow
result1 = expensive_computation(5)

# Second call - instant (cached)
result2 = expensive_computation(5)
✅ Pros
  • Add features without changing original
  • Combine multiple decorators flexibly
  • Single Responsibility - each decorator has one job
  • Can add/remove at runtime
❌ Cons
  • Many small classes can clutter code
  • Decorators aren't identical to wrapped object
  • Hard to remove specific decorator from stack

Facade Pattern

Intent: Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use. It's about simplifying complexity by hiding the details.

# Complex subsystem with many components
class CPU:
    def freeze(self): print("CPU: Freezing...")
    def jump(self, position): print(f"CPU: Jumping to {position}")
    def execute(self): print("CPU: Executing...")

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

class HardDrive:
    def read(self, lba, size):
        print(f"HDD: Reading {size} bytes from sector {lba}")
        return f"data_from_sector_{lba}"

# Facade simplifies the interface
class ComputerFacade:
    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.hard_drive = HardDrive()

    def start(self):
        """Simple interface hiding complex boot sequence"""
        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!")

# Client uses simple interface
computer = ComputerFacade()
computer.start()  # One call instead of 6!

# Real example: Home Theater System
class Amplifier:
    def on(self): print("Amp on")
    def set_volume(self, level): print(f"Volume: {level}")
    def off(self): print("Amp off")

class DVDPlayer:
    def on(self): print("DVD on")
    def play(self, movie): print(f"Playing {movie}")
    def stop(self): print("DVD stopped")
    def off(self): print("DVD off")

class Projector:
    def on(self): print("Projector on")
    def wide_screen_mode(self): print("Projector: widescreen")
    def off(self): print("Projector off")

class Lights:
    def dim(self, level): print(f"Lights dimmed to {level}%")

class HomeTheaterFacade:
    def __init__(self, amp, dvd, projector, lights):
        self.amp = amp
        self.dvd = dvd
        self.projector = projector
        self.lights = lights

    def watch_movie(self, movie):
        print("Get ready to watch a movie...")
        self.lights.dim(10)
        self.projector.on()
        self.projector.wide_screen_mode()
        self.amp.on()
        self.amp.set_volume(5)
        self.dvd.on()
        self.dvd.play(movie)

    def end_movie(self):
        print("Shutting down theater...")
        self.dvd.stop()
        self.dvd.off()
        self.amp.off()
        self.projector.off()
        self.lights.dim(100)

# Usage
theater = HomeTheaterFacade(
    Amplifier(), DVDPlayer(), Projector(), Lights()
)

theater.watch_movie("Inception")
# ... watch movie ...
theater.end_movie()
✅ Pros
  • Simplifies complex subsystems
  • Decouples client from implementation
  • Easier to use and understand
  • Can layer facades for different abstractions
❌ Cons
  • Can become a "god object" coupled to everything
  • May hide too much - limits advanced usage
  • Additional layer to maintain

Proxy Pattern

Intent: Provide a surrogate or placeholder for another object to control access to it. Useful for lazy initialization, access control, logging, caching, or remote object access.

# Subject interface
class Image(ABC):
    @abstractmethod
    def display(self):
        pass

# Real subject - expensive to create
class RealImage(Image):
    def __init__(self, filename):
        self.filename = filename
        self._load_from_disk()

    def _load_from_disk(self):
        print(f"Loading image from disk: {self.filename}")
        time.sleep(2)  # Simulate expensive operation

    def display(self):
        print(f"Displaying {self.filename}")

# Proxy - controls access to RealImage
class ImageProxy(Image):
    def __init__(self, filename):
        self.filename = filename
        self._real_image = None  # Lazy initialization

    def display(self):
        # Only load when actually needed
        if self._real_image is None:
            self._real_image = RealImage(self.filename)
        self._real_image.display()

# Usage - proxy delays expensive operation
print("Creating image proxy...")
image = ImageProxy("large_photo.jpg")  # Instant - no loading yet

print("First display call:")
image.display()  # Loads now (slow)

print("Second display call:")
image.display()  # Already loaded (fast)

# Protection Proxy - access control
class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            return True
        return False

    def deposit(self, amount):
        self.balance += amount

class BankAccountProxy:
    def __init__(self, account, user_role):
        self.account = account
        self.user_role = user_role

    def withdraw(self, amount):
        if self.user_role != "owner":
            raise PermissionError("Only owner can withdraw")
        return self.account.withdraw(amount)

    def deposit(self, amount):
        # Anyone can deposit
        return self.account.deposit(amount)

    def get_balance(self):
        if self.user_role not in ["owner", "accountant"]:
            raise PermissionError("Cannot view balance")
        return self.account.balance

# Usage
real_account = BankAccount(1000)

owner_proxy = BankAccountProxy(real_account, "owner")
owner_proxy.withdraw(100)  # ✓ Allowed

guest_proxy = BankAccountProxy(real_account, "guest")
try:
    guest_proxy.withdraw(100)  # ✗ Raises PermissionError
except PermissionError as e:
    print(f"Access denied: {e}")

# Caching Proxy
class ExpensiveService:
    def get_data(self, key):
        print(f"Fetching {key} from database...")
        time.sleep(1)
        return f"data_for_{key}"

class CachingProxy:
    def __init__(self, service):
        self.service = service
        self.cache = {}

    def get_data(self, key):
        if key not in self.cache:
            self.cache[key] = self.service.get_data(key)
        else:
            print(f"Returning cached data for {key}")
        return self.cache[key]

service = ExpensiveService()
proxy = CachingProxy(service)

proxy.get_data("user_123")  # Slow - fetches from DB
proxy.get_data("user_123")  # Fast - from cache
✅ Pros
  • Controls access to object transparently
  • Lazy initialization saves resources
  • Adds functionality without changing object
  • Open/Closed Principle
❌ Cons
  • Increases code complexity
  • May introduce latency
  • Response might be delayed (lazy loading)

Composite Pattern

Intent: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly. Perfect for representing hierarchies like file systems, UI components, or organizational charts.

💡 Key Insight: A file and a folder both can be treated as "FileSystemItem" - the folder just happens to contain other items. This uniformity is the power of Composite.

Structure: Component Hierarchy

from abc import ABC, abstractmethod

# Component - common interface
class Graphic(ABC):
    @abstractmethod
    def draw(self):
        pass

    @abstractmethod
    def get_size(self):
        pass

# Leaf - individual object
class Circle(Graphic):
    def __init__(self, radius):
        self.radius = radius

    def draw(self):
        print(f"Drawing circle with radius {self.radius}")

    def get_size(self):
        return self.radius * 2

class Square(Graphic):
    def __init__(self, side):
        self.side = side

    def draw(self):
        print(f"Drawing square with side {self.side}")

    def get_size(self):
        return self.side

# Composite - container of components
class CompositeGraphic(Graphic):
    def __init__(self, name="Group"):
        self.name = name
        self.children = []

    def add(self, graphic: Graphic):
        self.children.append(graphic)

    def remove(self, graphic: Graphic):
        self.children.remove(graphic)

    def draw(self):
        print(f"Drawing {self.name}:")
        for child in self.children:
            child.draw()

    def get_size(self):
        return sum(child.get_size() for child in self.children)

# Usage - treat individual and groups uniformly
circle = Circle(5)
square = Square(10)

# Create group
group1 = CompositeGraphic("Group 1")
group1.add(circle)
group1.add(square)

# Create larger group containing the first group
main_group = CompositeGraphic("Main Group")
main_group.add(group1)
main_group.add(Circle(3))

# Draw entire hierarchy
main_group.draw()
print(f"Total size: {main_group.get_size()}")

Real-World: File System

class FileSystemItem(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def get_size(self):
        pass

    @abstractmethod
    def display(self, indent=0):
        pass

# Leaf - File
class File(FileSystemItem):
    def __init__(self, name, size):
        super().__init__(name)
        self.size = size

    def get_size(self):
        return self.size

    def display(self, indent=0):
        print("  " * indent + f"📄 {self.name} ({self.size}KB)")

# Composite - Directory
class Directory(FileSystemItem):
    def __init__(self, name):
        super().__init__(name)
        self.items = []

    def add(self, item: FileSystemItem):
        self.items.append(item)

    def remove(self, item: FileSystemItem):
        self.items.remove(item)

    def get_size(self):
        return sum(item.get_size() for item in self.items)

    def display(self, indent=0):
        print("  " * indent + f"📁 {self.name}/")
        for item in self.items:
            item.display(indent + 1)

# Build file system structure
root = Directory("root")

home = Directory("home")
home.add(File("document.txt", 10))
home.add(File("photo.jpg", 500))

projects = Directory("projects")
projects.add(File("code.py", 25))
projects.add(File("README.md", 5))

home.add(projects)
root.add(home)
root.add(File("boot.ini", 2))

# Display entire tree
root.display()
print(f"\nTotal size: {root.get_size()}KB")

Notice how get_size() and display() work on both files and directories. The directory recursively calls these methods on its children - the client doesn't need to know the difference!

Real-World: Organization Chart

class Employee(ABC):
    def __init__(self, name, position, salary):
        self.name = name
        self.position = position
        self.salary = salary

    @abstractmethod
    def get_salary_cost(self):
        pass

    @abstractmethod
    def show_details(self, indent=0):
        pass

class Developer(Employee):
    def get_salary_cost(self):
        return self.salary

    def show_details(self, indent=0):
        print("  " * indent + f"👨‍💻 {self.name} - {self.position} (${self.salary})")

class Manager(Employee):
    def __init__(self, name, position, salary):
        super().__init__(name, position, salary)
        self.team = []

    def add_report(self, employee: Employee):
        self.team.append(employee)

    def remove_report(self, employee: Employee):
        self.team.remove(employee)

    def get_salary_cost(self):
        # Manager's salary + all team members
        team_cost = sum(member.get_salary_cost() for member in self.team)
        return self.salary + team_cost

    def show_details(self, indent=0):
        print("  " * indent + f"👔 {self.name} - {self.position} (${self.salary})")
        for member in self.team:
            member.show_details(indent + 1)

# Build organization
ceo = Manager("Alice", "CEO", 200000)

eng_manager = Manager("Bob", "Engineering Manager", 150000)
eng_manager.add_report(Developer("Charlie", "Senior Dev", 120000))
eng_manager.add_report(Developer("Diana", "Junior Dev", 80000))

sales_manager = Manager("Eve", "Sales Manager", 140000)
sales_manager.add_report(Developer("Frank", "Sales Rep", 70000))

ceo.add_report(eng_manager)
ceo.add_report(sales_manager)

# Display org chart and total cost
ceo.show_details()
print(f"\nTotal salary cost: ${ceo.get_salary_cost():,}")

When to Use Composite

  • Tree structures: When you need to represent part-whole hierarchies
  • Uniform treatment: When clients should ignore differences between compositions and individual objects
  • Recursive structures: File systems, UI widgets, scene graphs, organization charts
  • Menu systems: Menus containing menu items or sub-menus
✅ Pros
  • Simplifies client code - uniform interface
  • Easy to add new component types
  • Natural for tree structures
  • Recursive operations are straightforward
❌ Cons
  • Can make design overly general
  • Hard to restrict component types
  • May need type checking at runtime

⚡ BEHAVIORAL PATTERNS

Patterns for algorithms and responsibility assignment between objects

Observer Pattern

Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This is the foundation of event-driven programming and reactive systems.

# Subject (Observable)
class Subject:
    def __init__(self):
        self._observers = []
        self._state = None

    def attach(self, observer):
        if observer not in self._observers:
            self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, value):
        self._state = value
        self.notify()  # Notify all observers

# Observer interface
class Observer(ABC):
    @abstractmethod
    def update(self, subject):
        pass

# Concrete observers
class HexObserver(Observer):
    def update(self, subject):
        print(f"Hex: {hex(subject.state)}")

class BinaryObserver(Observer):
    def update(self, subject):
        print(f"Binary: {bin(subject.state)}")

class OctalObserver(Observer):
    def update(self, subject):
        print(f"Octal: {oct(subject.state)}")

# Usage
subject = Subject()

# Register observers
hex_obs = HexObserver()
bin_obs = BinaryObserver()
oct_obs = OctalObserver()

subject.attach(hex_obs)
subject.attach(bin_obs)
subject.attach(oct_obs)

# Change state - all observers notified automatically
subject.state = 15
# Hex: 0xf
# Binary: 0b1111
# Octal: 0o17

subject.state = 10
# Hex: 0xa
# Binary: 0b1010
# Octal: 0o12

Real-World: Stock Market

class Stock:
    def __init__(self, symbol, price):
        self.symbol = symbol
        self._price = price
        self._investors = []

    def attach(self, investor):
        self._investors.append(investor)

    def detach(self, investor):
        self._investors.remove(investor)

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        old_price = self._price
        self._price = value

        # Notify all investors of price change
        for investor in self._investors:
            investor.update(self, old_price, value)

class Investor:
    def __init__(self, name):
        self.name = name

    def update(self, stock, old_price, new_price):
        change = new_price - old_price
        direction = "up" if change > 0 else "down"
        print(f"{self.name} notified: {stock.symbol} went {direction} "
              f"from {old_price} to {new_price}")

# Usage
apple = Stock("AAPL", 150.00)

investor1 = Investor("Alice")
investor2 = Investor("Bob")

apple.attach(investor1)
apple.attach(investor2)

apple.price = 155.00  # Both investors notified
apple.price = 152.50  # Both investors notified

# Event system example
class EventManager:
    def __init__(self):
        self._subscribers = {}

    def subscribe(self, event_type, callback):
        if event_type not in self._subscribers:
            self._subscribers[event_type] = []
        self._subscribers[event_type].append(callback)

    def unsubscribe(self, event_type, callback):
        if event_type in self._subscribers:
            self._subscribers[event_type].remove(callback)

    def notify(self, event_type, data):
        if event_type in self._subscribers:
            for callback in self._subscribers[event_type]:
                callback(data)

# Usage
events = EventManager()

def on_user_login(data):
    print(f"User logged in: {data['username']}")

def send_welcome_email(data):
    print(f"Sending welcome email to {data['username']}")

def log_activity(data):
    print(f"Logging: User {data['username']} logged in at {data['time']}")

# Subscribe multiple handlers to same event
events.subscribe("user_login", on_user_login)
events.subscribe("user_login", send_welcome_email)
events.subscribe("user_login", log_activity)

# Trigger event - all handlers called
events.notify("user_login", {
    "username": "alice",
    "time": "2024-01-15 10:30:00"
})

The Observer pattern is fundamental to modern frameworks. React's state management, Vue's reactivity system, and event-driven architectures all use this pattern at their core.

✅ Pros
  • Loose coupling between subject and observers
  • Dynamic relationships - add/remove at runtime
  • Broadcast communication without hard dependencies
  • Foundation of event-driven systems
❌ Cons
  • Unexpected cascading updates
  • Memory leaks if observers not removed
  • No guarantee of notification order
  • Hard to debug complex event chains

Strategy Pattern

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. Perfect for when you have multiple ways to perform an operation.

# Strategy interface
class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

# Concrete strategies
class CreditCardStrategy(PaymentStrategy):
    def __init__(self, card_number, cvv):
        self.card_number = card_number
        self.cvv = cvv

    def pay(self, amount):
        print(f"Paid {amount} with credit card {self.card_number[-4:]}")

class PayPalStrategy(PaymentStrategy):
    def __init__(self, email):
        self.email = email

    def pay(self, amount):
        print(f"Paid {amount} through PayPal account {self.email}")

class CryptoStrategy(PaymentStrategy):
    def __init__(self, wallet_address):
        self.wallet_address = wallet_address

    def pay(self, amount):
        print(f"Paid {amount} in crypto to {self.wallet_address}")

# Context
class ShoppingCart:
    def __init__(self):
        self.items = []
        self.payment_strategy = None

    def add_item(self, item, price):
        self.items.append({"item": item, "price": price})

    def set_payment_strategy(self, strategy: PaymentStrategy):
        self.payment_strategy = strategy

    def checkout(self):
        total = sum(item["price"] for item in self.items)

        if not self.payment_strategy:
            raise ValueError("Payment strategy not set")

        self.payment_strategy.pay(total)
        self.items.clear()

# Usage - swap strategies at runtime
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)

# Pay with credit card
cart.set_payment_strategy(CreditCardStrategy("1234-5678-9012-3456", "123"))
cart.checkout()

# Next order - pay with PayPal
cart.add_item("Keyboard", 79.99)
cart.set_payment_strategy(PayPalStrategy("user@example.com"))
cart.checkout()

Real-World: Sorting Strategies

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data):
        pass

class QuickSort(SortStrategy):
    def sort(self, data):
        if len(data) <= 1:
            return data
        pivot = data[len(data) // 2]
        left = [x for x in data if x < pivot]
        middle = [x for x in data if x == pivot]
        right = [x for x in data if x > pivot]
        return self.sort(left) + middle + self.sort(right)

class BubbleSort(SortStrategy):
    def sort(self, data):
        data = data.copy()
        n = len(data)
        for i in range(n):
            for j in range(0, n - i - 1):
                if data[j] > data[j + 1]:
                    data[j], data[j + 1] = data[j + 1], data[j]
        return data

class MergeSort(SortStrategy):
    def sort(self, data):
        if len(data) <= 1:
            return data

        mid = len(data) // 2
        left = self.sort(data[:mid])
        right = self.sort(data[mid:])

        return self._merge(left, right)

    def _merge(self, left, right):
        result = []
        i = j = 0

        while i < len(left) and j < len(right):
            if left[i] < right[j]:
                result.append(left[i])
                i += 1
            else:
                result.append(right[j])
                j += 1

        result.extend(left[i:])
        result.extend(right[j:])
        return result

class DataSorter:
    def __init__(self, strategy: SortStrategy):
        self.strategy = strategy

    def sort(self, data):
        return self.strategy.sort(data)

# Usage - choose strategy based on data characteristics
data = [64, 34, 25, 12, 22, 11, 90]

# Small dataset - any algorithm works
sorter = DataSorter(QuickSort())
print(sorter.sort(data))

# Large dataset - use efficient algorithm
large_data = list(range(10000, 0, -1))
sorter = DataSorter(MergeSort())
sorted_data = sorter.sort(large_data)

# Compression strategies
class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, data): pass

class ZipCompression(CompressionStrategy):
    def compress(self, data):
        return f"ZIP compressed: {data}"

class RARCompression(CompressionStrategy):
    def compress(self, data):
        return f"RAR compressed: {data}"

class Compressor:
    def __init__(self, strategy: CompressionStrategy):
        self.strategy = strategy

    def compress_file(self, filename):
        with open(filename, 'r') as f:
            data = f.read()
        return self.strategy.compress(data)

# Choose compression based on requirements
compressor = Compressor(ZipCompression())
result = compressor.compress_file("document.txt")
✅ Pros
  • Swap algorithms at runtime
  • Isolate algorithm implementation details
  • Replace inheritance with composition
  • Open/Closed Principle - easy to add strategies
❌ Cons
  • Clients must know strategy differences
  • Increases number of objects
  • Overkill if algorithms rarely change

Command Pattern

Intent: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. Decouples the object that invokes the operation from the one that knows how to perform it.

# Command interface
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass

# Receiver - knows how to perform operations
class Light:
    def on(self):
        print("Light is ON")

    def off(self):
        print("Light is OFF")

# Concrete commands
class LightOnCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self):
        self.light.on()

    def undo(self):
        self.light.off()

class LightOffCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self):
        self.light.off()

    def undo(self):
        self.light.on()

# Invoker
class RemoteControl:
    def __init__(self):
        self.history = []

    def execute_command(self, command: Command):
        command.execute()
        self.history.append(command)

    def undo_last(self):
        if self.history:
            command = self.history.pop()
            command.undo()

# Usage
light = Light()
remote = RemoteControl()

# Turn light on
remote.execute_command(LightOnCommand(light))

# Turn light off
remote.execute_command(LightOffCommand(light))

# Undo - turns light back on
remote.undo_last()

Real-World: Text Editor

class TextEditor:
    def __init__(self):
        self.text = ""

    def write(self, text):
        self.text += text

    def delete(self, length):
        self.text = self.text[:-length]

    def get_text(self):
        return self.text

class WriteCommand(Command):
    def __init__(self, editor: TextEditor, text: str):
        self.editor = editor
        self.text = text

    def execute(self):
        self.editor.write(self.text)

    def undo(self):
        self.editor.delete(len(self.text))

class DeleteCommand(Command):
    def __init__(self, editor: TextEditor, length: int):
        self.editor = editor
        self.length = length
        self.deleted_text = None

    def execute(self):
        # Save text before deleting for undo
        self.deleted_text = self.editor.get_text()[-self.length:]
        self.editor.delete(self.length)

    def undo(self):
        self.editor.write(self.deleted_text)

class EditorInvoker:
    def __init__(self):
        self.history = []
        self.redo_stack = []

    def execute(self, command: Command):
        command.execute()
        self.history.append(command)
        self.redo_stack.clear()  # Clear redo stack on new command

    def undo(self):
        if self.history:
            command = self.history.pop()
            command.undo()
            self.redo_stack.append(command)

    def redo(self):
        if self.redo_stack:
            command = self.redo_stack.pop()
            command.execute()
            self.history.append(command)

# Usage
editor = TextEditor()
invoker = EditorInvoker()

# Type some text
invoker.execute(WriteCommand(editor, "Hello "))
invoker.execute(WriteCommand(editor, "World"))
print(editor.get_text())  # "Hello World"

# Delete some text
invoker.execute(DeleteCommand(editor, 5))
print(editor.get_text())  # "Hello "

# Undo delete
invoker.undo()
print(editor.get_text())  # "Hello World"

# Undo write
invoker.undo()
print(editor.get_text())  # "Hello "

# Redo
invoker.redo()
print(editor.get_text())  # "Hello World"
✅ Pros
  • Decouples sender from receiver
  • Single Responsibility - one command, one action
  • Undo/redo functionality
  • Delay or queue command execution
  • Compose commands into complex operations
❌ Cons
  • Many command classes can add complexity
  • Additional layer between sender and receiver
  • Can be overkill for simple operations

State Pattern

Intent: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class. Eliminates large conditional statements and makes state transitions explicit.

# State interface
class State(ABC):
    @abstractmethod
    def insert_coin(self, machine):
        pass

    @abstractmethod
    def eject_coin(self, machine):
        pass

    @abstractmethod
    def turn_crank(self, machine):
        pass

    @abstractmethod
    def dispense(self, machine):
        pass

# Concrete states
class NoCoinState(State):
    def insert_coin(self, machine):
        print("Coin inserted")
        machine.set_state(machine.has_coin_state)

    def eject_coin(self, machine):
        print("No coin to eject")

    def turn_crank(self, machine):
        print("Insert coin first")

    def dispense(self, machine):
        print("Pay first")

class HasCoinState(State):
    def insert_coin(self, machine):
        print("Coin already inserted")

    def eject_coin(self, machine):
        print("Coin ejected")
        machine.set_state(machine.no_coin_state)

    def turn_crank(self, machine):
        print("Crank turned")
        machine.set_state(machine.sold_state)

    def dispense(self, machine):
        print("Turn crank first")

class SoldState(State):
    def insert_coin(self, machine):
        print("Please wait, dispensing")

    def eject_coin(self, machine):
        print("Too late, already turning")

    def turn_crank(self, machine):
        print("Turning twice doesn't get you another item")

    def dispense(self, machine):
        machine.release_item()
        if machine.count > 0:
            machine.set_state(machine.no_coin_state)
        else:
            print("Out of items")
            machine.set_state(machine.sold_out_state)

class SoldOutState(State):
    def insert_coin(self, machine):
        print("Sold out, can't accept coin")

    def eject_coin(self, machine):
        print("No coin to eject")

    def turn_crank(self, machine):
        print("Sold out")

    def dispense(self, machine):
        print("No items to dispense")

# Context
class VendingMachine:
    def __init__(self, count):
        self.no_coin_state = NoCoinState()
        self.has_coin_state = HasCoinState()
        self.sold_state = SoldState()
        self.sold_out_state = SoldOutState()

        self.count = count
        self.state = self.sold_out_state if count == 0 else self.no_coin_state

    def set_state(self, state):
        self.state = state

    def insert_coin(self):
        self.state.insert_coin(self)

    def eject_coin(self):
        self.state.eject_coin(self)

    def turn_crank(self):
        self.state.turn_crank(self)
        self.state.dispense(self)

    def release_item(self):
        print("Item released")
        self.count -= 1

# Usage
machine = VendingMachine(2)

machine.insert_coin()  # Coin inserted
machine.turn_crank()   # Crank turned, Item released

machine.insert_coin()  # Coin inserted
machine.turn_crank()   # Crank turned, Item released, Out of items

machine.insert_coin()  # Sold out, can't accept coin

Real-World: Order Processing

class OrderState(ABC):
    @abstractmethod
    def confirm(self, order): pass

    @abstractmethod
    def ship(self, order): pass

    @abstractmethod
    def deliver(self, order): pass

    @abstractmethod
    def cancel(self, order): pass

class PendingState(OrderState):
    def confirm(self, order):
        print("Order confirmed")
        order.state = ConfirmedState()

    def ship(self, order):
        print("Cannot ship - order not confirmed")

    def deliver(self, order):
        print("Cannot deliver - order not confirmed")

    def cancel(self, order):
        print("Order cancelled")
        order.state = CancelledState()

class ConfirmedState(OrderState):
    def confirm(self, order):
        print("Order already confirmed")

    def ship(self, order):
        print("Order shipped")
        order.state = ShippedState()

    def deliver(self, order):
        print("Cannot deliver - not shipped yet")

    def cancel(self, order):
        print("Order cancelled")
        order.state = CancelledState()

class ShippedState(OrderState):
    def confirm(self, order):
        print("Order already confirmed")

    def ship(self, order):
        print("Order already shipped")

    def deliver(self, order):
        print("Order delivered")
        order.state = DeliveredState()

    def cancel(self, order):
        print("Cannot cancel - already shipped")

class DeliveredState(OrderState):
    def confirm(self, order):
        print("Order already delivered")

    def ship(self, order):
        print("Order already delivered")

    def deliver(self, order):
        print("Order already delivered")

    def cancel(self, order):
        print("Cannot cancel - already delivered")

class CancelledState(OrderState):
    def confirm(self, order):
        print("Cannot confirm - order cancelled")

    def ship(self, order):
        print("Cannot ship - order cancelled")

    def deliver(self, order):
        print("Cannot deliver - order cancelled")

    def cancel(self, order):
        print("Order already cancelled")

class Order:
    def __init__(self, order_id):
        self.order_id = order_id
        self.state = PendingState()

    def confirm(self):
        self.state.confirm(self)

    def ship(self):
        self.state.ship(self)

    def deliver(self):
        self.state.deliver(self)

    def cancel(self):
        self.state.cancel(self)

# Usage
order = Order("ORD-123")

order.confirm()  # Order confirmed
order.ship()     # Order shipped
order.deliver()  # Order delivered
order.cancel()   # Cannot cancel - already delivered
✅ Pros
  • Eliminates complex conditionals
  • Clean state transitions
  • Easy to add new states
  • Single Responsibility - one class per state
  • Makes state transitions explicit
❌ Cons
  • Overkill for simple state machines
  • Many state classes to maintain
  • State transition logic spread across classes

Chain of Responsibility Pattern

Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

# Handler interface
class Handler(ABC):
    def __init__(self):
        self._next_handler = None

    def set_next(self, handler):
        self._next_handler = handler
        return handler

    @abstractmethod
    def handle(self, request):
        if self._next_handler:
            return self._next_handler.handle(request)
        return None

# Concrete handlers
class AuthenticationHandler(Handler):
    def handle(self, request):
        if not request.get("authenticated"):
            return "Authentication failed"
        print("Authentication passed")
        return super().handle(request)

class AuthorizationHandler(Handler):
    def handle(self, request):
        if not request.get("authorized"):
            return "Authorization failed"
        print("Authorization passed")
        return super().handle(request)

class ValidationHandler(Handler):
    def handle(self, request):
        if not request.get("data"):
            return "Validation failed - no data"
        print("Validation passed")
        return super().handle(request)

class ProcessingHandler(Handler):
    def handle(self, request):
        print("Processing request")
        return "Request processed successfully"

# Build the chain
auth = AuthenticationHandler()
authz = AuthorizationHandler()
validation = ValidationHandler()
processing = ProcessingHandler()

auth.set_next(authz).set_next(validation).set_next(processing)

# Test requests
print("Request 1:")
request1 = {"authenticated": True, "authorized": True, "data": "test"}
result = auth.handle(request1)
print(f"Result: {result}")

print("Request 2:")
request2 = {"authenticated": True, "authorized": False, "data": "test"}
result = auth.handle(request2)
print(f"Result: {result}")

# Real-world: Logging levels
class Logger(Handler):
    def __init__(self, level):
        super().__init__()
        self.level = level

    def handle(self, request):
        if request["level"] <= self.level:
            self.write_message(request["message"])
        return super().handle(request)

    @abstractmethod
    def write_message(self, message):
        pass

class ConsoleLogger(Logger):
    def write_message(self, message):
        print(f"Console: {message}")

class FileLogger(Logger):
    def write_message(self, message):
        print(f"File: {message}")

class EmailLogger(Logger):
    def write_message(self, message):
        print(f"Email: {message}")

# Log levels
INFO = 1
WARNING = 2
ERROR = 3

# Setup chain
console = ConsoleLogger(INFO)
file = FileLogger(WARNING)
email = EmailLogger(ERROR)

console.set_next(file).set_next(email)

# Log messages
console.handle({"level": INFO, "message": "Info message"})
console.handle({"level": WARNING, "message": "Warning message"})
console.handle({"level": ERROR, "message": "Error message"})
✅ Pros
  • Decouples sender from receivers
  • Single Responsibility - each handler one task
  • Open/Closed - easy to add new handlers
  • Control order of request handling
❌ Cons
  • No guarantee request will be handled
  • Can be hard to debug the chain
  • Performance concerns with long chains

Template Method Pattern

Intent: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

class DataMiner(ABC):
    """Template method defines the algorithm structure"""

    def mine(self, path):
        file = self.open_file(path)
        raw_data = self.extract_data(file)
        data = self.parse_data(raw_data)
        analysis = self.analyze_data(data)
        self.send_report(analysis)
        self.close_file(file)

    @abstractmethod
    def open_file(self, path):
        pass

    @abstractmethod
    def extract_data(self, file):
        pass

    def parse_data(self, raw_data):
        # Default implementation - can be overridden
        return raw_data.split('\n')

    @abstractmethod
    def analyze_data(self, data):
        pass

    def send_report(self, analysis):
        # Hook - optional override
        print(f"Report: {analysis}")

    @abstractmethod
    def close_file(self, file):
        pass

class PDFDataMiner(DataMiner):
    def open_file(self, path):
        print(f"Opening PDF file: {path}")
        return f"pdf_file_{path}"

    def extract_data(self, file):
        print("Extracting data from PDF")
        return "pdf data content"

    def analyze_data(self, data):
        print("Analyzing PDF data")
        return {"type": "pdf", "lines": len(data)}

    def close_file(self, file):
        print("Closing PDF file")

class CSVDataMiner(DataMiner):
    def open_file(self, path):
        print(f"Opening CSV file: {path}")
        return f"csv_file_{path}"

    def extract_data(self, file):
        print("Extracting data from CSV")
        return "csv,data,content"

    def parse_data(self, raw_data):
        # Override default parsing
        return raw_data.split(',')

    def analyze_data(self, data):
        print("Analyzing CSV data")
        return {"type": "csv", "columns": len(data)}

    def close_file(self, file):
        print("Closing CSV file")

# Usage
pdf_miner = PDFDataMiner()
pdf_miner.mine("report.pdf")

print("\n")

csv_miner = CSVDataMiner()
csv_miner.mine("data.csv")

# Real-world: Game AI
class GameAI(ABC):
    def turn(self):
        """Template method for AI turn"""
        self.collect_resources()
        self.build_structures()
        self.build_units()
        self.attack()

    def collect_resources(self):
        print("Collecting resources")

    @abstractmethod
    def build_structures(self):
        pass

    @abstractmethod
    def build_units(self):
        pass

    def attack(self):
        # Hook - can be overridden
        print("Default attack")

class AggressiveAI(GameAI):
    def build_structures(self):
        print("Building barracks")

    def build_units(self):
        print("Building many attack units")

    def attack(self):
        print("Aggressive attack!")

class DefensiveAI(GameAI):
    def build_structures(self):
        print("Building walls and towers")

    def build_units(self):
        print("Building defensive units")

    def attack(self):
        print("Defensive posture - only counter-attack")

aggressive = AggressiveAI()
aggressive.turn()

print("\n")

defensive = DefensiveAI()
defensive.turn()
✅ Pros
  • Reuse common algorithm structure
  • Control which steps subclasses can override
  • Reduces code duplication
  • Hooks for optional extension points
❌ Cons
  • Clients limited by skeleton algorithm
  • Violates Liskov Substitution if not careful
  • Harder to maintain as steps increase

Iterator Pattern

Intent: Provide a way to access elements of a collection sequentially without exposing its underlying representation. Separates the traversal logic from the collection itself, allowing multiple simultaneous traversals.

💡 Python Built-in: Python's iterator protocol (__iter__ and__next__) makes this pattern native to the language. Understanding iterators is essential for mastering Python!

Basic Implementation

class BooksCollection:
    def __init__(self):
        self._books = []

    def add_book(self, book):
        self._books.append(book)

    def __iter__(self):
        """Return iterator object"""
        return BookIterator(self._books)

class BookIterator:
    def __init__(self, books):
        self._books = books
        self._index = 0

    def __iter__(self):
        """Iterator must return itself"""
        return self

    def __next__(self):
        """Return next item or raise StopIteration"""
        if self._index < len(self._books):
            book = self._books[self._index]
            self._index += 1
            return book
        raise StopIteration

# Usage - works with for loops automatically
collection = BooksCollection()
collection.add_book("The Hobbit")
collection.add_book("1984")
collection.add_book("Dune")

for book in collection:
    print(book)

Python Generator - Cleaner Approach

class BooksCollection:
    def __init__(self):
        self._books = []

    def add_book(self, book):
        self._books.append(book)

    def __iter__(self):
        """Generator - much simpler!"""
        for book in self._books:
            yield book

    def reverse_iter(self):
        """Custom iterator - reverse order"""
        for book in reversed(self._books):
            yield book

    def genre_filter(self, genre):
        """Filtered iterator"""
        for book in self._books:
            if book.genre == genre:
                yield book

# Usage
collection = BooksCollection()
# ... add books ...

# Forward iteration
for book in collection:
    print(book)

# Reverse iteration
for book in collection.reverse_iter():
    print(book)

The generator approach (using yield) is the Pythonic way to create iterators. It's more concise and handles the iteration state automatically.

Real-World: Binary Tree Traversal

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

class BinaryTree:
    def __init__(self, root_value):
        self.root = TreeNode(root_value)

    def inorder_traversal(self):
        """Left -> Root -> Right"""
        def traverse(node):
            if node:
                yield from traverse(node.left)
                yield node.value
                yield from traverse(node.right)
        return traverse(self.root)

    def preorder_traversal(self):
        """Root -> Left -> Right"""
        def traverse(node):
            if node:
                yield node.value
                yield from traverse(node.left)
                yield from traverse(node.right)
        return traverse(self.root)

    def postorder_traversal(self):
        """Left -> Right -> Root"""
        def traverse(node):
            if node:
                yield from traverse(node.left)
                yield from traverse(node.right)
                yield node.value
        return traverse(self.root)

    def level_order_traversal(self):
        """Breadth-first traversal"""
        queue = [self.root]
        while queue:
            node = queue.pop(0)
            if node:
                yield node.value
                queue.append(node.left)
                queue.append(node.right)

# Build tree
tree = BinaryTree(1)
tree.root.left = TreeNode(2)
tree.root.right = TreeNode(3)
tree.root.left.left = TreeNode(4)
tree.root.left.right = TreeNode(5)

# Different traversal strategies
print("Inorder:", list(tree.inorder_traversal()))
print("Preorder:", list(tree.preorder_traversal()))
print("Postorder:", list(tree.postorder_traversal()))
print("Level-order:", list(tree.level_order_traversal()))

Real-World: Paginated API Results

class PaginatedAPI:
    def __init__(self, url, page_size=10):
        self.url = url
        self.page_size = page_size

    def __iter__(self):
        """Iterate through all pages automatically"""
        page = 1
        while True:
            # Simulate API call
            results = self._fetch_page(page)

            if not results:
                break

            for item in results:
                yield item

            page += 1

    def _fetch_page(self, page):
        # Simulate fetching data from API
        # In reality, this would make HTTP request
        print(f"Fetching page {page}...")
        if page > 3:  # Stop after 3 pages
            return []
        return [f"Item {i}" for i in range((page-1)*self.page_size, page*self.page_size)]

# Usage - client doesn't worry about pagination!
api = PaginatedAPI("https://api.example.com/items", page_size=5)

for item in api:
    print(item)
    # Process each item without pagination logic

When to Use Iterator

  • Hide complexity: Complex data structure traversal (trees, graphs, composite structures)
  • Multiple traversals: Need different ways to iterate over same collection
  • Lazy loading: Don't want to load entire collection into memory
  • Uniform interface: Provide consistent iteration across different collections
  • Streaming data: Process infinite or large datasets incrementally
✅ Pros
  • Simplifies collection interface
  • Multiple simultaneous traversals
  • Supports lazy evaluation
  • Memory efficient for large datasets
  • Python native with generators
❌ Cons
  • Overkill for simple collections
  • Can be less efficient than direct access
  • One-way traversal (typically)
✨ Python Pro Tip: Use generators (yield) for most iterator needs. They're more concise, memory-efficient, and Pythonic than implementing the full iterator protocol with __iter__ and __next__.

Choosing the Right Pattern

ProblemPatternKey Benefit
Need exactly one instance of a classSingletonControlled access to sole instance
Creating objects without specifying exact classFactory MethodDecouples creation from usage
Complex object with many parametersBuilderClear, fluent construction
Incompatible interfaces need to work togetherAdapterBridges incompatible interfaces
Add responsibilities without inheritanceDecoratorFlexible, composable enhancements
Simplify complex subsystemFacadeUnified, simple interface
Control access to expensive objectProxyLazy loading, access control
One-to-many change notificationObserverLoose coupling, event systems
Swap algorithms at runtimeStrategyFlexible algorithm selection
Encapsulate actions as objectsCommandUndo/redo, queuing, logging
Behavior changes with stateStateClean state transitions
Multiple objects can handle requestChain of ResponsibilityDecoupled request handling
Algorithm steps vary by subclassTemplate MethodReuse algorithm structure

Pattern Relationships: Understanding the Differences

Many patterns appear similar at first glance. Understanding their key differences helps you choose the right tool for the job.

🔄 Strategy vs State
Strategy Pattern
  • Intent: Make algorithms interchangeable
  • Client knows: Which strategy to use
  • Strategies: Independent and stateless
  • Example: Payment methods (credit card, PayPal, crypto)
State Pattern
  • Intent: Change behavior based on state
  • Client knows: Nothing - state changes automatically
  • States: Know about transitions
  • Example: Order lifecycle (pending, shipped, delivered)
🎨 Decorator vs Proxy
Decorator Pattern
  • Intent: Add new functionality
  • Composition: Multiple decorators stack
  • Object: Created beforehand
  • Example: Coffee + milk + sugar + whipped cream
Proxy Pattern
  • Intent: Control access to object
  • Composition: Usually single proxy
  • Object: May not exist yet (lazy loading)
  • Example: Image placeholder that loads on demand
🏭 Factory Method vs Abstract Factory
Factory Method
  • Creates: One product
  • Inheritance: Subclass decides which class
  • Complexity: Simpler - one method
  • Example: DocumentCreator creates one document type
Abstract Factory
  • Creates: Families of related products
  • Composition: Factory object creates products
  • Complexity: More complex - multiple products
  • Example: GUIFactory creates button + checkbox + input
🔌 Adapter vs Facade
Adapter Pattern
  • Intent: Convert one interface to another
  • Wraps: Single class
  • When: After system is designed
  • Example: XML to JSON converter
Facade Pattern
  • Intent: Simplify complex subsystem
  • Wraps: Multiple classes
  • When: During system design
  • Example: Home theater system controller
⚡ Command vs Strategy
Command Pattern
  • Intent: Encapsulate requests as objects
  • Focus: When and how to execute
  • Features: Undo/redo, queueing, logging
  • Example: Text editor operations (write, delete)
Strategy Pattern
  • Intent: Encapsulate algorithms
  • Focus: Which algorithm to use
  • Features: Swappable algorithms
  • Example: Sorting algorithms (quick, merge, bubble)
📋 Template Method vs Strategy
Template Method
  • Uses: Inheritance (extends base class)
  • Flexibility: Static - defined at class creation
  • Changes: Steps of algorithm
  • Example: DataMiner with different file formats
Strategy
  • Uses: Composition (has-a relationship)
  • Flexibility: Dynamic - change at runtime
  • Changes: Entire algorithm
  • Example: Payment processor with different methods
💡 Key Insight: These patterns solve different problems, even when implementations look similar. Always focus on the intent - what problem are you trying to solve? - rather than the structure of the code.

Common Pitfalls & Anti-Patterns

❌ Pattern Obsession

Forcing patterns where they don't belong. Not every problem needs a design pattern. Start simple, refactor to patterns when complexity demands it.

❌ Bad - Over-engineered:
# For 2 greeting types!
class GreetingStrategy(ABC):
    def greet(self): pass

class FormalGreeting(GreetingStrategy):
    def greet(self): return "Hello"

class CasualGreeting(GreetingStrategy):
    def greet(self): return "Hi"

greeter = Greeter(FormalGreeting())
✅ Good - Simple:
# Just use a function or dict
greetings = {
    "formal": "Hello",
    "casual": "Hi"
}

def greet(style):
    return greetings.get(style, "Hello")
❌ Premature Optimization

Adding patterns "just in case" leads to over-engineered code. Apply patterns when you have a concrete need, not speculatively.

❌ Bad - "Just in case":
# Adding factory for ONE logger type
class LoggerFactory(ABC):
    def create_logger(self): pass

class ConsoleLoggerFactory(LoggerFactory):
    def create_logger(self):
        return ConsoleLogger()

# Maybe we'll add FileLogger later?
logger = factory.create_logger()
✅ Good - Start simple:
# Use it directly until you need more
logger = ConsoleLogger()

# Refactor to Factory WHEN you add
# FileLogger, DatabaseLogger, etc.
# (YAGNI - You Aren't Gonna Need It)
❌ Wrong Pattern Selection

Using Strategy when you need State, or Decorator when you need Adapter. Understand the intent of each pattern, not just the implementation.

❌ Bad - Using Strategy for state transitions:
# States that transition are NOT strategies!
order = Order()
order.set_strategy(PendingStrategy())
# Client must manually manage transitions
order.set_strategy(ShippedStrategy())
order.set_strategy(DeliveredStrategy())
✅ Good - Use State pattern:
# State pattern handles transitions
order = Order()  # Starts in Pending
order.ship()     # Auto-transitions to Shipped
order.deliver()  # Auto-transitions to Delivered
# States know how to transition!
❌ Ignoring Language Features

Python has built-in features (decorators, context managers, duck typing) that can replace some patterns. Use the language idiomatically.

❌ Bad - Verbose Decorator class:
# Decorator pattern the hard way
class Component(ABC):
    def operation(self): pass

class ConcreteComponent(Component):
    def operation(self): return "data"

class LoggingDecorator(Component):
    def __init__(self, component):
        self._component = component
    def operation(self):
        print("Logging...")
        return self._component.operation()
✅ Good - Python decorator:
# Use Python's @ syntax
def log_calls(func):
    def wrapper(*args, **kwargs):
        print("Logging...")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def get_data():
    return "data"
⚠️ Remember: Design patterns are tools, not goals. The goal is clean, maintainable code. If a pattern doesn't improve your code, don't use it. Simplicity beats cleverness.

Modern Considerations

Patterns in Modern Python

  • First-class functions: Strategy pattern is often just passing functions
  • Decorators: Python's @ syntax implements Decorator pattern elegantly
  • Context managers: With statement replaces explicit try/finally
  • Duck typing: Reduces need for explicit interfaces
  • Metaclasses: Can implement Singleton and other creation patterns
  • Dataclasses: Simplify Builder pattern for simple cases

Patterns and Modern Architecture

  • Microservices: Factory and Adapter patterns for service integration
  • Event-driven: Observer pattern is fundamental to reactive systems
  • Functional programming: Some OO patterns less relevant in FP
  • Async/await: New patterns emerging for concurrent programming

Key Takeaways

  • Design patterns are proven solutions to recurring problems, not code to copy-paste
  • Patterns provide vocabulary for communicating complex designs concisely
  • Creational patterns handle object creation flexibility
  • Structural patterns help compose objects into larger structures
  • Behavioral patterns focus on communication between objects
  • Combine patterns to solve complex problems - they work together
  • Know when NOT to use patterns - simplicity beats cleverness
  • Adapt patterns to your language - Python has idioms that replace some patterns
  • Patterns are guidelines, not rules - understand the intent and adapt to your context