Design Patterns (Gang of Four) Cheat Sheet
Summarizes the classic Gang of Four creational, structural, and behavioral design patterns with a runnable Strategy and Singleton example.
Creational Patterns
Patterns concerned with flexible object creation.
- Singleton- Ensures a class has only one instance and provides a global point of access to it
- Factory Method- Defines an interface for creating an object, letting subclasses decide which concrete class to instantiate
- Abstract Factory- Provides an interface for creating families of related objects without specifying their concrete classes
- Builder- Separates construction of a complex object from its representation, allowing step-by-step assembly
- Prototype- Creates new objects by cloning an existing instance instead of instantiating from scratch
Structural Patterns
Patterns for composing classes and objects into larger structures.
- Adapter- Converts the interface of a class into another interface that clients expect
- Decorator- Attaches additional responsibilities to an object dynamically without altering its class
- Facade- Provides a simplified, unified interface to a complex subsystem
- Composite- Composes objects into tree structures so clients treat individual objects and compositions uniformly
- Proxy- Provides a surrogate or placeholder for another object to control access to it
- Bridge- Decouples an abstraction from its implementation so the two can vary independently
Behavioral Patterns
Patterns focused on communication and responsibility between objects.
- Strategy- Defines a family of interchangeable algorithms and encapsulates each one behind a common interface
- Observer- Defines a one-to-many dependency so that when one object changes state, all its dependents are notified
- Command- Encapsulates a request as an object, allowing parameterization, queuing, and undoable operations
- Template Method- Defines the skeleton of an algorithm in a base class, deferring specific steps to subclasses
- Iterator- Provides a way to access elements of a collection sequentially without exposing its underlying representation
- State- Allows an object to alter its behavior when its internal state changes, appearing to change its class
Strategy & Singleton in Practice
Two of the most commonly used GoF patterns implemented in Python.
# Strategy patternclass DiscountStrategy: def apply(self, price: float) -> float: raise NotImplementedErrorclass NoDiscount(DiscountStrategy): def apply(self, price): return priceclass PercentageDiscount(DiscountStrategy): def __init__(self, percent): self.percent = percent def apply(self, price): return price * (1 - self.percent / 100)class Order: def __init__(self, strategy: DiscountStrategy): self.strategy = strategy def total(self, price): return self.strategy.apply(price)order = Order(PercentageDiscount(10))order.total(100) # => 90.0# Singleton patternclass Config: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance
Observer & Decorator in Practice
A pub/sub Observer paired with a stacked Decorator, showing how the two behavioral/structural patterns compose cleanly.
from abc import ABC, abstractmethod# Observer patternclass Subject: def __init__(self): self._observers = [] def subscribe(self, fn): self._observers.append(fn) def notify(self, event): for fn in self._observers: fn(event)class StockTicker(Subject): def update_price(self, symbol, price): self.notify({"symbol": symbol, "price": price})ticker = StockTicker()ticker.subscribe(lambda e: print(f"Logger: {e}"))ticker.subscribe(lambda e: print(f"Alert if {e['price']} > 100"))ticker.update_price("ACME", 105)# Decorator pattern (function-based, stackable)class Coffee(ABC): @abstractmethod def cost(self) -> float: ...class Espresso(Coffee): def cost(self): return 2.0class MilkDecorator(Coffee): def __init__(self, wrapped: Coffee): self._wrapped = wrapped def cost(self): return self._wrapped.cost() + 0.5class SyrupDecorator(Coffee): def __init__(self, wrapped: Coffee): self._wrapped = wrapped def cost(self): return self._wrapped.cost() + 0.3drink = SyrupDecorator(MilkDecorator(Espresso()))drink.cost() # => 2.8, decorators stack in wrap order
Visitor Pattern & Double Dispatch
Adding new operations over a fixed object hierarchy without modifying the element classes, using double dispatch.
interface Shape { void accept(ShapeVisitor v);}class Circle implements Shape { double radius; Circle(double r) { radius = r; } public void accept(ShapeVisitor v) { v.visit(this); } // double dispatch}class Rectangle implements Shape { double w, h; Rectangle(double w, double h) { this.w = w; this.h = h; } public void accept(ShapeVisitor v) { v.visit(this); }}interface ShapeVisitor { void visit(Circle c); void visit(Rectangle r);}class AreaVisitor implements ShapeVisitor { double total = 0; public void visit(Circle c) { total += Math.PI * c.radius * c.radius; } public void visit(Rectangle r) { total += r.w * r.h; }}// New operations (e.g. PerimeterVisitor, SvgExportVisitor) are added// without touching Circle or Rectangle -- Visitor trades ease of adding// new element types for ease of adding new operations.
Chain of Responsibility
Decoupling a request's sender from its handlers by letting each link in a chain decide to process or forward it.
abstract class Handler { protected next?: Handler; setNext(h: Handler): Handler { this.next = h; return h; } handle(req: Request): Response | null { return this.next ? this.next.handle(req) : null; }}class AuthHandler extends Handler { handle(req: Request) { if (!req.token) return { status: 401 }; return super.handle(req); }}class RateLimitHandler extends Handler { handle(req: Request) { if (req.ip in blocked) return { status: 429 }; return super.handle(req); }}class RouteHandler extends Handler { handle(req: Request) { return { status: 200, body: route(req) }; }}const chain = new AuthHandler();chain.setNext(new RateLimitHandler()).setNext(new RouteHandler());chain.handle(incomingRequest);// Each handler decides independently whether to short-circuit or delegate
Flyweight for Memory-Heavy Object Graphs
Sharing immutable intrinsic state across many logical instances to cut memory use in large object graphs like text renderers or game maps.
class GlyphFlyweight: """Intrinsic (shared) state: everything that doesn't vary per occurrence.""" def __init__(self, char, font, size): self.char, self.font, self.size = char, font, size def render(self, x, y): # extrinsic state passed in at call time print(f"draw '{self.char}' ({self.font},{self.size}) at ({x},{y})")class GlyphFactory: _pool = {} @classmethod def get(cls, char, font, size): key = (char, font, size) if key not in cls._pool: cls._pool[key] = GlyphFlyweight(char, font, size) return cls._pool[key]# Rendering a 10,000-character document reuses ~100 unique glyph objects# instead of allocating 10,000 -- extrinsic (x, y) state stays outside# the shared flyweight.
Common GoF Anti-Patterns & Gotchas
Ways these patterns get misapplied in real codebases.
- Singleton as global mutable state- Turns a Singleton into a hidden dependency that breaks unit test isolation; prefer dependency injection of a single instance instead
- Factory explosion- Wrapping every constructor in a Factory Method 'for consistency' adds indirection with no variability to hide
- Visitor vs Strategy confusion- Visitor adds operations across a fixed type hierarchy; Strategy swaps one algorithm at a single call site -- don't reach for Visitor when Strategy suffices
- Decorator ordering bugs- Stacked decorators are order-sensitive (e.g. compression before encryption vs. after); document the required wrap order
- Abstract Factory over-generalization- Building a family-of-products factory before a second product family actually exists is speculative complexity
- Command without undo- If a Command's execute() has no matching undo(), you're just reinventing a callback with extra ceremony
- Proxy hiding failures- A caching or remote Proxy that silently swallows errors from the real subject makes debugging much harder
Favor composition-based patterns (Strategy, Decorator) over inheritance-based ones when behavior needs to vary at runtime — they keep classes open for extension without deep, fragile class hierarchies.