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
⚡ Behavioral
Purpose: Object interaction
🏗️ 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.
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
✅ 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 airSimple 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")✅ 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, baconAdvanced: 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 10kw_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.
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 platformReal-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.
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), redReal-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.
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: 0o12Real-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 coinReal-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.
__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 logicWhen 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)
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
| Problem | Pattern | Key Benefit |
|---|---|---|
| Need exactly one instance of a class | Singleton | Controlled access to sole instance |
| Creating objects without specifying exact class | Factory Method | Decouples creation from usage |
| Complex object with many parameters | Builder | Clear, fluent construction |
| Incompatible interfaces need to work together | Adapter | Bridges incompatible interfaces |
| Add responsibilities without inheritance | Decorator | Flexible, composable enhancements |
| Simplify complex subsystem | Facade | Unified, simple interface |
| Control access to expensive object | Proxy | Lazy loading, access control |
| One-to-many change notification | Observer | Loose coupling, event systems |
| Swap algorithms at runtime | Strategy | Flexible algorithm selection |
| Encapsulate actions as objects | Command | Undo/redo, queuing, logging |
| Behavior changes with state | State | Clean state transitions |
| Multiple objects can handle request | Chain of Responsibility | Decoupled request handling |
| Algorithm steps vary by subclass | Template Method | Reuse 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
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.
# 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())# 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.
# 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()# 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.
# 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())
# 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.
# 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()# 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"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