R0KernelPath
module 252 / 262

Design Patterns

The 23 Gang of Four design patterns are the shared vocabulary of professional software engineering — named, reusable solutions to recurring design problems that embody decades of wisdom about making code flexible, testable, and maintainable.

~18h focused study11 concepts

Overview

Design patterns are reusable solutions to recurring problems in software design. They are not algorithms or libraries you can copy-paste — they are templates for thinking: named, battle-tested shapes of collaboration between objects that experienced engineers reach for when they recognize a familiar problem structure.

The canonical catalogue comes from the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — universally called the "Gang of Four" (GoF). Their 23 patterns were distilled from years of observing what good Smalltalk and C++ programs had in common, and they remain the shared vocabulary of professional software engineering today. When a senior engineer says "use a Factory here" or "that's screaming for an Observer," they are drawing on this vocabulary to communicate a structural intent instantly and precisely.

The patterns divide into three families. Creational patterns govern how objects come into existence — abstracting the instantiation process so clients don't depend on concrete classes. Structural patterns govern how objects and classes are composed into larger structures — building flexible, efficient assemblies from parts. Behavioral patterns govern how objects communicate and distribute responsibility — defining the choreography of a collaboration without tightly coupling the dancers.

Understanding design patterns does two things simultaneously: it trains you to recognize recurring design problems quickly (pattern matching in your own mental model), and it gives you a vocabulary to discuss and document your solutions with other engineers. But patterns also carry a risk: premature or dogmatic application produces over-engineered, unreadable code. The master skill is knowing not just what a pattern is, but when it earns its complexity cost — and when a simpler design is better. This module teaches both sides.

why this matters for the kernel

Design patterns sit at the intersection of theory and craft. Theoretically, they embody deep principles of object-oriented design — the SOLID principles, the Open/Closed Principle, Dependency Inversion — in concrete, teachable form. When you understand why the Strategy pattern exists, you understand why switching on type tags in a long if-chain is fragile, and why polymorphism exists in the first place. Patterns are the practical side of the programming-languages insight that "programs should be written for people to read, and only incidentally for machines to execute."

In real systems, design patterns appear everywhere. The Django web framework's ORM uses the Active Record and Repository patterns. React's component model is deeply informed by Composite and Observer. The Java standard library ships canonical Decorator (I/O streams), Iterator (collections), and Command (Swing actions). Every game engine uses Component/Entity systems descended from GoF Composite. Understanding patterns lets you read large codebases faster — you recognize the shape of a 2,000-line subsystem as "oh, this is a Chain of Responsibility feeding into a Command queue" — and it lets you communicate architectural decisions in a sentence rather than a paragraph. Equally important, recognizing anti-patterns (Blob classes, Lava Flow, God Objects) lets you diagnose why a codebase is painful to work in and chart a refactoring path toward something better.

GoF Pattern Taxonomy and Key Relationships

rendering diagram…

The 23 GoF patterns grouped by purpose (Creational / Structural / Behavioral), with the Strategy and Observer patterns shown in structural detail. Strategy shows composition over inheritance: Context holds a Strategy interface reference, swapping algorithms at runtime. Observer shows the publish-subscribe relationship: Subject holds a list of Observer interfaces, notifying all without knowing concrete types.

The concepts

01The Pattern Vocabulary: Why Named Patterns Matter

Before diving into specific patterns, it is worth understanding why naming matters at all. A pattern is not just a technique — it is a shared vocabulary entry in the language of software design.

Imagine two engineers reviewing code. Engineer A says: "I think we should add a class that sits between the client and this subsystem, translates the client's calls into the subsystem's API, and hides the complexity." Engineer B says: "Use a Facade." Same idea, one word. The named pattern carries not just the structural description but a whole package of associated knowledge: known trade-offs, known pitfalls, known variations, known contexts where it works well or poorly.

GoF patterns have three parts that matter:

  1. Name: the vocabulary entry. Memorizing it is genuinely useful.
  2. Intent: a one-sentence description of the problem it solves. This is what you pattern-match against.
  3. Structure: the class diagram showing participants and their relationships.

The GoF catalogue divides patterns along two axes:

  • Purpose: Creational / Structural / Behavioral
  • Scope: Class-level (uses inheritance, static) / Object-level (uses composition, dynamic)

The overwhelming majority are object-level, reflecting the GoF's core principle: favor object composition over class inheritance. Inheritance creates compile-time coupling between parent and child; composition allows runtime flexibility because you can swap the composed object.

A critical meta-point: patterns describe solutions in a context, not just solutions. The same structural arrangement might be a Strategy in one context and a State in another, because the intent and the reason the code changes differs.

02Creational Patterns: Factory Method and Abstract Factory

The core problem: client code that uses new ConcreteClass() is tightly coupled to that class. If you ever need a different implementation — for testing, for a different platform, for a different configuration — you must change the client.

Factory Method solves this by defining an interface for creating an object but letting subclasses decide which class to instantiate. The creator class has an abstract create_product() method; concrete subclasses override it to return the appropriate product. The client calls create_product() without knowing or caring what concrete type comes back.

from abc import ABC, abstractmethod

class Dialog(ABC):           # Creator
    def render(self):
        btn = self.create_button()   # factory method
        btn.on_click(self.close_dialog)
        btn.render()

    @abstractmethod
    def create_button(self): ...   # Factory Method

class WindowsDialog(Dialog):       # Concrete Creator
    def create_button(self):
        return WindowsButton()

class WebDialog(Dialog):           # Concrete Creator
    def create_button(self):
        return HTMLButton()

The client chooses a Dialog subclass at startup (based on OS / config) and then calls render() without caring about buttons.

Abstract Factory goes one level up: it provides an interface for creating families of related objects, without specifying their concrete classes. Think of a UI toolkit factory that creates matching buttons, checkboxes, and text fields all in the same visual style.

class GUIFactory(ABC):
    @abstractmethod
    def create_button(self): ...
    @abstractmethod
    def create_checkbox(self): ...

class MacFactory(GUIFactory):
    def create_button(self): return MacButton()
    def create_checkbox(self): return MacCheckbox()

class WinFactory(GUIFactory):
    def create_button(self): return WinButton()
    def create_checkbox(self): return WinCheckbox()

The client receives a factory, uses it, and the entire family is consistent. Adding a new platform means adding a new factory; existing client code doesn't change. This is the Open/Closed Principle in action.

When to use: when the code must be independent of how its objects are created; when you need to provide a library of products and reveal only interfaces, not implementations; when a system should be configured with one of multiple families of products.

# Abstract Factory in ~20 lines
class GUIFactory(ABC):
    @abstractmethod
    def create_button(self): ...
    @abstractmethod
    def create_checkbox(self): ...

class MacFactory(GUIFactory):
    def create_button(self): return MacButton()
    def create_checkbox(self): return MacCheckbox()

class Application:
    def __init__(self, factory: GUIFactory):
        self.button = factory.create_button()
        self.checkbox = factory.create_checkbox()

# Swap entire UI family by changing one line:
app = Application(MacFactory())   # or WinFactory()

03Creational Patterns: Singleton, Builder, and Prototype

Singleton ensures a class has exactly one instance and provides a global access point to it. Classic use cases: a logging service, a thread pool, a configuration manager.

class Logger:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

Every call to Logger() returns the same object. Warning: Singleton is the most controversial GoF pattern. It introduces global state, makes unit testing hard (you can't easily substitute a mock), creates hidden coupling between classes, and is notoriously hard to get right in multithreaded code without synchronization. Modern practice often replaces Singleton with dependency injection — the single instance is created once at the composition root and injected wherever needed, giving the same sharing benefit without the global-state pathology.

Builder separates the construction of a complex object from its representation so the same construction process can create different representations. Imagine building a House: it has walls, roof, windows, foundation — each step optional or variant. Rather than a constructor with 12 parameters, a Builder provides step-by-step methods:

class QueryBuilder:
    def __init__(self): self._parts = {}
    def select(self, *cols): self._parts['cols'] = cols; return self
    def from_table(self, t): self._parts['table'] = t; return self
    def where(self, cond): self._parts['cond'] = cond; return self
    def build(self):
        cols = ', '.join(self._parts.get('cols', ['*']))
        sql = f"SELECT {cols} FROM {self._parts['table']}"
        if 'cond' in self._parts:
            sql += f" WHERE {self._parts['cond']}"
        return sql

query = QueryBuilder().select('id','name').from_table('users').where('age > 18').build()

The fluent interface (method chaining with return self) is characteristic. Builder is especially valuable when: the object has many optional parameters; the construction order matters; you want to produce different representations (e.g., JSON vs SQL) with the same steps.

Prototype creates new objects by cloning an existing instance (the prototype). This is useful when creating an object from scratch is expensive (e.g., loading a large configuration from disk) but cloning a pre-initialized instance is cheap. Python's copy.deepcopy() is the standard mechanism. The key insight: the client doesn't need to know the concrete class of the object being cloned — it just calls clone() on the prototype interface.

import copy

class DocumentTemplate:
    def __init__(self, title, sections):
        self.title = title
        self.sections = sections   # potentially expensive to build

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

template = DocumentTemplate("Report", ["Introduction", "Analysis"])
doc1 = template.clone()
doc1.title = "Q1 Report"
doc2 = template.clone()
doc2.title = "Q2 Report"
# template is untouched; doc1 and doc2 are independent

04Structural Patterns: Adapter, Decorator, and Facade

Structural patterns are about composition — building larger structures from existing pieces, often to bridge mismatches between what you have and what you need.

Adapter (also called Wrapper) converts the interface of a class into another interface that clients expect. It's the software equivalent of a power plug adapter: you have a device (the Adaptee) with one interface and a socket (the Target) expecting another.

class EuropeanSocket:        # Adaptee: provides 220V
    def provide_220v(self): return 220

class USSocket:              # Target interface
    def provide_110v(self): ...

class PowerAdapter(USSocket):  # Adapter
    def __init__(self, eu_socket):
        self.eu_socket = eu_socket
    def provide_110v(self):    # adapts the call
        return self.eu_socket.provide_220v() // 2

Adapters are the standard solution when integrating third-party libraries with incompatible interfaces, or when you want to reuse existing classes without modifying them (respecting the Open/Closed Principle).

Decorator attaches additional responsibilities to an object dynamically — a flexible alternative to subclassing. The key structural insight: a Decorator implements the same interface as the component it wraps, holds a reference to a component, and delegates calls to it while adding behavior before and/or after.

Python's I/O system is a perfect example: BufferedReader(FileReader(file)) — each wrapper adds a layer of behavior without changing the wrapped object. Python's @decorator syntax is exactly this pattern. Java's I/O streams (new BufferedInputStream(new FileInputStream(path))) are the textbook example.

class Coffee:                       # Component interface
    def cost(self): return 1.0
    def description(self): return "Coffee"

class MilkDecorator:                # Decorator
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self): return self._coffee.cost() + 0.25
    def description(self): return self._coffee.description() + ", Milk"

class SugarDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self): return self._coffee.cost() + 0.10
    def description(self): return self._coffee.description() + ", Sugar"

drink = SugarDecorator(MilkDecorator(Coffee()))
print(drink.cost())         # 1.35
print(drink.description())  # Coffee, Milk, Sugar

Facade provides a simplified interface to a complex subsystem. It doesn't hide the subsystem — you can still use it directly — but it provides a convenient high-level entry point for common use cases. Think of a Compiler class that wraps Lexer, Parser, SemanticAnalyzer, CodeGenerator, and Linker. Client code calling compiler.compile(source) doesn't need to know about all those steps.

Facade is perhaps the most commonly used pattern in large systems, because complexity tends to grow and Facades manage that complexity at subsystem boundaries.

# Decorator pattern: stackable logging and caching
class DataService:
    def fetch(self, key): return f"data:{key}"

class LoggingDecorator:
    def __init__(self, service):
        self._service = service
    def fetch(self, key):
        print(f"Fetching {key}")
        result = self._service.fetch(key)
        print(f"Got {result}")
        return result

class CachingDecorator:
    def __init__(self, service):
        self._service = service
        self._cache = {}
    def fetch(self, key):
        if key not in self._cache:
            self._cache[key] = self._service.fetch(key)
        return self._cache[key]

# Stack: caching wraps logging wraps raw service
service = CachingDecorator(LoggingDecorator(DataService()))

05Structural Patterns: Composite, Bridge, Proxy, and Flyweight

Composite lets you compose objects into tree structures to represent part-whole hierarchies, and lets clients treat individual objects and compositions uniformly. The classic example is a file system: both File and Directory have size() and display() methods; a Directory delegates to its children, which may themselves be directories.

class FileSystemItem(ABC):
    @abstractmethod
    def size(self): ...

class File(FileSystemItem):
    def __init__(self, name, sz): self.name=name; self._sz=sz
    def size(self): return self._sz

class Directory(FileSystemItem):
    def __init__(self, name): self.name=name; self.children=[]
    def add(self, item): self.children.append(item)
    def size(self): return sum(c.size() for c in self.children)

The power: code that calls item.size() doesn't care whether item is a leaf or a subtree. This uniformity makes recursive tree operations clean.

Bridge decouples an abstraction from its implementation so both can vary independently. The classic example: shapes (abstraction) rendered by different renderers (implementation). Without Bridge, you'd need CircleOpenGL, CircleVulkan, SquareOpenGL, SquareVulkan — a Cartesian product of classes. Bridge breaks this by composing a Shape object with a Renderer object: Circle(OpenGLRenderer()). The shape defines what to draw; the renderer defines how.

Proxy provides a surrogate or placeholder for another object to control access to it. Three main uses:

  • Virtual Proxy: delays expensive creation until first use (lazy initialization). A document editor might hold a proxy for a large image; the real image is only loaded when displayed.
  • Protection Proxy: controls access based on permissions.
  • Remote Proxy: represents an object in a different address space (this is the foundation of RPC).
class RealImage:
    def __init__(self, filename):
        self.filename = filename
        self._load()  # expensive
    def _load(self): print(f"Loading {self.filename}")
    def display(self): print(f"Displaying {self.filename}")

class ImageProxy:           # Virtual Proxy
    def __init__(self, filename):
        self.filename = filename
        self._real = None
    def display(self):
        if self._real is None:
            self._real = RealImage(self.filename)  # lazy
        self._real.display()

Flyweight uses sharing to efficiently support a large number of fine-grained objects. The key insight: split object state into intrinsic (shared, immutable) and extrinsic (unique per use, passed in). A text editor might have millions of character objects; the Flyweight stores only the glyph shape (intrinsic) in a shared pool, while position and formatting (extrinsic) are passed on each render call. This can reduce memory from gigabytes to megabytes.

# Flyweight: shared character glyphs in a text editor
class GlyphFlyweight:
    # intrinsic state: the character shape
    def __init__(self, char): self.char = char
    def render(self, x, y, font_size):  # extrinsic state passed in
        print(f"Render '{self.char}' at ({x},{y}) size {font_size}")

class GlyphFactory:
    _pool = {}
    @classmethod
    def get(cls, char):
        if char not in cls._pool:
            cls._pool[char] = GlyphFlyweight(char)
        return cls._pool[char]  # shared instance

# 1000 'A' characters share ONE GlyphFlyweight('A') object
glyphs = [GlyphFactory.get('A') for _ in range(1000)]
glyphs[0].render(10, 20, 12)

06Behavioral Patterns: Observer and Strategy

Behavioral patterns are about algorithms and the assignment of responsibilities between objects. They describe not just objects and classes, but the patterns of communication between them.

Observer (also called Publish-Subscribe or Event-Listener) defines a one-to-many dependency: when one object (the Subject/Publisher) changes state, all its dependents (Observers/Subscribers) are notified and updated automatically.

This is one of the most widely used patterns in all of software. Every GUI framework uses it (button click → listeners). Every reactive programming model is built on it. MVC/MVP architectures depend on it (Model notifies View).

class EventEmitter:         # Subject
    def __init__(self):
        self._listeners = {}
    def on(self, event, fn):
        self._listeners.setdefault(event, []).append(fn)
    def emit(self, event, data=None):
        for fn in self._listeners.get(event, []):
            fn(data)

stock = EventEmitter()
stock.on('price_change', lambda p: print(f"Dashboard: {p}"))
stock.on('price_change', lambda p: print(f"Alert if p>{100}: {p > 100}"))
stock.emit('price_change', 105)

The Subject doesn't know who its observers are — it just knows they implement a callback interface. Observers can be added or removed at runtime. This is loose coupling: Subject and Observer only depend on the abstract event interface, not on each other's concrete implementations.

Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.

The motivating problem: you have a class that needs to sort data, but sometimes you want quicksort, sometimes mergesort, sometimes a GPU sort. If you use an if-chain in the class, it violates the Open/Closed Principle — adding a new sort requires changing the class. Strategy extracts each algorithm into its own class behind a common interface, and the context holds a reference to a strategy object.

class Sorter:
    def __init__(self, strategy): self._strategy = strategy
    def sort(self, data): return self._strategy(data)

# Strategies are just callables here — Python functions are Strategy objects
quick_sorter = Sorter(sorted)          # stdlib TimSort
custom_sorter = Sorter(lambda d: sorted(d, key=lambda x: -x))  # reverse

print(quick_sorter.sort([3,1,2]))   # [1, 2, 3]
print(custom_sorter.sort([3,1,2]))  # [3, 2, 1]

In Python and other languages with first-class functions, Strategy often collapses to simply passing a function — the pattern's intent is preserved (swappable algorithm) even if the class structure is simplified. This is a good example of patterns being language-dependent in their expression.

# Observer: stock price feed with multiple listeners
class StockFeed:
    def __init__(self, symbol):
        self.symbol = symbol
        self._price = 0.0
        self._observers = []

    def subscribe(self, observer): self._observers.append(observer)
    def unsubscribe(self, observer): self._observers.remove(observer)

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

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

    def _notify(self):
        for obs in self._observers:
            obs.update(self.symbol, self._price)

class PriceAlert:
    def __init__(self, threshold): self.threshold = threshold
    def update(self, sym, price):
        if price > self.threshold:
            print(f"ALERT: {sym} at {price} > {self.threshold}")

feed = StockFeed("AAPL")
feed.subscribe(PriceAlert(150))
feed.price = 155  # triggers alert

07Behavioral Patterns: Command, Iterator, and Template Method

Command encapsulates a request as an object, thereby allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. The Command object has an execute() method and (for undo) an undo() method. It typically stores the receiver (the object that does the work) and the parameters of the operation.

Real-world uses: GUI action systems (every menu item is a Command), macro recorders, transaction logs, job queues, undo/redo stacks.

class TextEditor:
    def __init__(self): self.text = ""
    def insert(self, s): self.text += s
    def delete(self, n): self.text = self.text[:-n]

class InsertCommand:
    def __init__(self, editor, text):
        self.editor = editor; self.text = text
    def execute(self): self.editor.insert(self.text)
    def undo(self): self.editor.delete(len(self.text))

history = []
editor = TextEditor()
cmd = InsertCommand(editor, "Hello")
cmd.execute(); history.append(cmd)  # text = "Hello"
cmd2 = InsertCommand(editor, " World")
cmd2.execute(); history.append(cmd2)  # text = "Hello World"
history.pop().undo()  # text = "Hello" again

Iterator provides a way to access elements of a collection sequentially without exposing its underlying representation. This pattern is so fundamental that most modern languages build it into the language itself (Python's for x in collection, Java's Iterable, C++'s range-based for). The pattern separates the traversal algorithm from the collection structure, so you can have multiple traversal algorithms (breadth-first, depth-first, reverse) for the same collection without bloating the collection class.

Template Method defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. The base class calls abstract methods that subclasses implement — the algorithm's structure is fixed, only the steps vary. This is the Hollywood Principle: "Don't call us, we'll call you" — the base class calls the subclass, not the other way around.

class DataMiner(ABC):          # Template Method pattern
    def mine(self, path):      # Template method — fixed skeleton
        data = self.extract(path)   # Step 1 — abstract
        parsed = self.parse(data)   # Step 2 — abstract
        analysis = self.analyze(parsed)  # Step 3 — concrete (default)
        return self.report(analysis)     # Step 4 — concrete (default)

    @abstractmethod
    def extract(self, path): ...
    @abstractmethod
    def parse(self, data): ...

    def analyze(self, data): return data  # default: no-op
    def report(self, analysis): return analysis  # default: pass-through

class CSVMiner(DataMiner):
    def extract(self, path): return open(path).read()
    def parse(self, data): return [line.split(',') for line in data.splitlines()]

Template Method is related to Strategy, but uses inheritance rather than composition. GoF advice: prefer Strategy (composition) when the entire algorithm varies; use Template Method when only steps of the algorithm vary.

# Command pattern with undo stack
from collections import deque

class Command(ABC):
    @abstractmethod
    def execute(self): ...
    @abstractmethod
    def undo(self): ...

class UndoableEditor:
    def __init__(self):
        self.text = ""
        self._history = deque(maxlen=100)

    def do(self, command: Command):
        command.execute()
        self._history.append(command)

    def undo(self):
        if self._history:
            self._history.pop().undo()

08Behavioral Patterns: Chain of Responsibility, State, Visitor, Mediator, Memento, Interpreter

The remaining six behavioral patterns each address a distinct communication problem:

Chain of Responsibility passes a request along a chain of handlers, where each handler either processes it or passes it to the next. This decouples sender from receiver. Classic use: middleware pipelines (HTTP request processing in Express.js/Django), logging systems (DEBUG → INFO → WARN → ERROR handlers), permission checks.

class Handler(ABC):
    def __init__(self): self._next = None
    def set_next(self, handler): self._next = handler; return handler
    def handle(self, req):
        if self._next: return self._next.handle(req)

class AuthHandler(Handler):
    def handle(self, req):
        if not req.get('auth'): return "401 Unauthorized"
        return super().handle(req)

class RateLimitHandler(Handler):
    def handle(self, req):
        if req.get('rate_exceeded'): return "429 Too Many Requests"
        return super().handle(req)

State allows an object to alter its behavior when its internal state changes. The object appears to change its class. Instead of a large switch-on-state in every method, each state is a class implementing the state interface. A TrafficLight with states Red, Yellow, Green each knows what to do on transition().

Visitor lets you add new operations to an existing class hierarchy without modifying the classes. The trick: each element has an accept(visitor) method that calls visitor.visit(self). New operations become new Visitor classes. The downside: adding new element types requires changing all visitors.

Mediator reduces chaotic dependencies between objects by making them communicate through a central mediator object. Air traffic control is the classic analogy: planes don't communicate with each other directly, they communicate through the control tower. In GUIs, a dialog can act as a mediator between its components, so a checkbox enabling a text field doesn't depend directly on the text field.

Memento captures and externalizes an object's internal state so it can be restored later, without violating encapsulation. The object (Originator) creates a Memento containing a snapshot of its state; a Caretaker stores the Memento. Undo systems often use Memento for state that is complex to reconstruct from a command log.

Interpreter defines a grammar for a language and provides an interpreter to deal with that grammar. Each grammar rule becomes a class. This pattern is specialized and relatively rare — you'll see it in scripting engines, regular expression matchers, and query parsers. For non-trivial languages, parser generators (ANTLR, Yacc) are used instead.

# State pattern: simple vending machine
class VendingMachine:
    def __init__(self):
        self.state = IdleState()

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

class IdleState:
    def insert_coin(self, machine):
        print("Coin inserted")
        machine.state = HasCoinState()
    def select_item(self, machine): print("Insert coin first")
    def dispense(self, machine): print("Insert coin first")

class HasCoinState:
    def insert_coin(self, machine): print("Coin already inserted")
    def select_item(self, machine):
        print("Item selected")
        machine.state = DispensingState()
    def dispense(self, machine): print("Select item first")

class DispensingState:
    def insert_coin(self, machine): print("Wait, dispensing")
    def select_item(self, machine): print("Already dispensing")
    def dispense(self, machine):
        print("Dispensed!")
        machine.state = IdleState()

09SOLID Principles: The Theoretical Foundation of Patterns

Design patterns are practical solutions, but they rest on a theoretical foundation: the SOLID principles (named by Robert Martin). Understanding SOLID explains why patterns are structured the way they are.

S — Single Responsibility Principle: A class should have only one reason to change. If a class handles both business logic and database persistence, it has two reasons to change (logic changes, schema changes). Patterns like Repository, Facade, and Command help enforce SRP by separating concerns into distinct classes.

O — Open/Closed Principle: Software entities should be open for extension but closed for modification. Adding new behavior should not require changing existing code. Strategy, Decorator, and Factory Method are the canonical examples: you add a new Strategy class rather than modifying the context; you add a new Decorator rather than modifying the component.

L — Liskov Substitution Principle: Subtypes must be substitutable for their base types. If you have a function that works with Shape, it must work with any Shape subclass without knowing the difference. Violations (like a Square that breaks Rectangle's invariants) indicate a design problem that patterns like Bridge (separating abstraction from implementation) can address.

I — Interface Segregation Principle: Clients should not be forced to depend on interfaces they don't use. A fat interface with 20 methods should be split into smaller, more focused interfaces. This principle guides how you define the interfaces in Adapter, Observer, and Visitor patterns.

D — Dependency Inversion Principle: High-level modules should not depend on low-level modules; both should depend on abstractions. This is the theoretical foundation for almost every GoF pattern. Factory Method, Abstract Factory, and Dependency Injection all implement DIP: the client depends on an abstraction (an interface), not on a concrete class.

When you see a design pattern, ask: which SOLID principle does it enforce? The answer reveals the essential insight of the pattern.

# Dependency Inversion Principle in action
# Bad: high-level module depends on low-level concrete class
class ReportGenerator:
    def generate(self, data):
        db = MySQLDatabase()      # tight coupling!
        return db.query(data)

# Good: both depend on abstraction
class Database(ABC):
    @abstractmethod
    def query(self, sql): ...

class ReportGenerator:
    def __init__(self, db: Database):  # inject abstraction
        self.db = db
    def generate(self, data):
        return self.db.query(data)     # loose coupling

# Can swap MySQL for PostgreSQL, SQLite, or a mock in tests
# without changing ReportGenerator at all

10Anti-Patterns: What Not to Do and Why

An anti-pattern is a commonly used but counterproductive solution to a recurring problem — the wrong answer that looks reasonable and is reached again and again. Recognizing anti-patterns is as important as knowing patterns.

God Object / Blob: A single class that knows too much or does too much. It accumulates responsibilities that should be spread across many classes. The symptom: a class with hundreds of methods and thousands of lines. The fix: apply SRP, extract classes, use patterns like Facade (to hide complexity), Strategy (to extract algorithms), and Observer (to decouple event handling).

Lava Flow: Dead code that can't be removed because nobody knows what it does or whether something depends on it. It solidifies into the codebase like cooling lava. Prevention: write tests, document, refactor continuously.

Golden Hammer: When you have a favorite tool (or pattern), everything looks like a nail. Overusing Singleton for shared state, or wrapping everything in Factories and Strategies when a simple function would do. The fix: match the solution to the actual problem, not to your familiarity.

Spaghetti Code: Control flow that is so tangled (deeply nested conditionals, global state mutation, goto-style logic) that it cannot be followed. The fix: extract methods, apply Command (to encapsulate actions), apply State (to replace conditionals on state).

Premature Abstraction / Over-Engineering: Creating pattern structures before you need them. A Factory is unnecessary overhead when you have exactly one product. An Abstract Factory is unnecessary when products don't come in families. The right time to introduce a pattern is when you feel the pain its absence causes — when you have two concrete implementations and need to swap them, when you have five similar conditionals that keep changing together. The principle: three strikes and you refactor (the Rule of Three: the first time you write something, you write it; the second time, you write it again; the third time, you refactor).

Pattern Blindness: Never seeing when a pattern applies. A 500-line method with 15 if-elseif branches on a type tag is begging for Strategy or Visitor. Recognizing this requires pattern-matching experience, which comes from studying and practicing patterns.

The meta-lesson: patterns are tools, not rules. The goal is readable, maintainable, testable code. If a pattern helps achieve that, use it. If it adds complexity without benefit, skip it.

# Anti-pattern: God Object
class Application:
    def login(self, user, pw): ...
    def hash_password(self, pw): ...
    def send_welcome_email(self, user): ...
    def render_dashboard(self): ...
    def fetch_user_from_db(self, id): ...
    def update_user_in_db(self, user): ...
    def log_event(self, event): ...
    # ... 47 more methods

# Refactored: responsibilities separated
class AuthService:
    def login(self, user, pw): ...
    def hash_password(self, pw): ...

class UserRepository:
    def find(self, id): ...
    def save(self, user): ...

class EmailService:
    def send_welcome(self, user): ...

class Logger:
    def log(self, event): ...

11Refactoring Toward Patterns

Patterns should rarely be designed in from scratch. The healthy workflow is: write simple code first, refactor toward patterns when the pain appears. This is the essence of Martin Fowler's Refactoring book and Kent Beck's test-driven development philosophy.

Here are the most common "smells" and the patterns that cure them:

Code SmellRefactoring DirectionPattern Applied
Switch/if-chain on typeReplace conditional with polymorphismStrategy, State
Duplicate code across subclassesPull common structure upTemplate Method
Client creates objects directlyExtract factoryFactory Method
Tight coupling to concrete classIntroduce interface + injectDependency Injection, Strategy
Object does too muchExtract class, delegateFacade, Command
Cross-cutting concerns (logging, auth)Wrap with behaviorDecorator, Proxy
Objects need change notificationsIntroduce event mechanismObserver
Complex constructionExtract builderBuilder

The Strangler Fig pattern (not GoF, but important) is a macro-level refactoring: you gradually replace a legacy system by building the new system alongside it, intercepting requests at the boundary (via Proxy or Adapter) and routing them to the new system as it is built. When the new system handles everything, you remove the old.

The refactoring cycle:

  1. Write a failing test that exposes the smell.
  2. Make the test pass with a minimal fix.
  3. Refactor: introduce the pattern.
  4. Verify the tests still pass.
  5. Commit.

This cycle is important: never refactor without tests. Patterns often involve adding indirection (new classes, new interfaces), which is easy to get wrong. Tests are your safety net.

# Before: switch on type — brittle, violates OCP
def process_shape(shape):
    if shape['type'] == 'circle':
        return 3.14159 * shape['r'] ** 2
    elif shape['type'] == 'rectangle':
        return shape['w'] * shape['h']
    elif shape['type'] == 'triangle':
        return 0.5 * shape['b'] * shape['h']
    # Adding a new shape requires modifying THIS function

# After: Strategy/Polymorphism — open for extension
class Shape(ABC):
    @abstractmethod
    def area(self): ...

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return 3.14159 * self.r ** 2

class Rectangle(Shape):
    def __init__(self, w, h): self.w=w; self.h=h
    def area(self): return self.w * self.h

def process_shape(shape: Shape):
    return shape.area()  # works for ANY Shape, now or future

Key insights

  • ✦Favor object composition over class inheritance: composition is more flexible (runtime swappable), while inheritance creates compile-time coupling that is hard to change.
  • ✦A pattern is defined by its intent, not its structure: Strategy and State have nearly identical class diagrams but solve different problems — what varies and why reveals which pattern applies.
  • ✦Patterns are language-dependent in expression: in Python, Strategy often collapses to a passed function; in Java it requires a full interface and class hierarchy. The pattern's intent persists even when the structure simplifies.
  • ✦The Open/Closed Principle is the design force behind most patterns: Factory Method, Strategy, Decorator, and Observer all allow extension without modification of existing code.
  • ✦Dependency Inversion (depend on abstractions, not concretions) is the theoretical heart of creational patterns: Factory Method, Abstract Factory, and Dependency Injection all make high-level modules independent of low-level implementations.
  • ✦Premature application of patterns is an anti-pattern itself: add a pattern when you feel the pain its absence causes — the Rule of Three is a practical trigger.
  • ✦The GoF catalogue has 23 patterns, but five dominate real-world code: Observer, Strategy, Decorator, Factory Method, and Singleton (despite its drawbacks). Master these five first.
  • ✦Refactoring toward patterns is safer than designing them in upfront: write simple code first, let the design breathe, introduce patterns when structural complexity or duplication appears.

Common pitfalls

  • ⚠Applying Singleton everywhere for shared state: Singleton introduces hidden global state that makes testing hard and creates coupling. Use dependency injection instead — create one instance at the composition root and pass it where needed.
  • ⚠Confusing Decorator with inheritance: Decorator wraps an object that implements the same interface; it does not subclass the component. If your Decorator inherits from the component class AND holds a reference to it, you have a design smell.
  • ⚠Using Factory Method when a simple constructor suffices: factories add indirection cost; only use them when you need to defer the choice of concrete class or when subclasses must control object creation.
  • ⚠Treating Observer as synchronous when it should be async: a Subject calling observers in a tight loop can cause performance issues or reentrancy bugs if observers modify the Subject. Consider event queues for complex observer graphs.
  • ⚠Confusing Strategy and State: both allow behavioral variation, but Strategy encapsulates an algorithm the client selects; State encapsulates behavior that changes automatically based on the object's internal condition.
  • ⚠Over-using Template Method and locking in an inheritance hierarchy: prefer Strategy (composition) when the algorithm itself varies; use Template Method only when the skeleton is fixed and only small steps vary.
  • ⚠Writing Composite without a common interface for leaves and composites: if clients must type-check to distinguish leaves from branches, the uniformity benefit of Composite is lost.
  • ⚠Implementing patterns without understanding the problem: copying a pattern structure without understanding its intent produces cargo-cult code that has all the complexity but none of the benefit.

Practice

  1. 01
    easy

    Implement a plugin system for a text editor using the Strategy pattern. The editor should support multiple syntax highlighting strategies (Python, JavaScript, plain text). Users can switch strategies at runtime. Write the interface, three concrete strategies, and a test that verifies swapping strategies changes the output without modifying the editor class.

  2. 02
    medium

    Design and implement a logging framework using the Decorator pattern. Support these orthogonal concerns that should be stackable in any combination: timestamping, log-level filtering, output to file, and output to console. A single log message should flow through all active decorators. Write tests showing that combinations like TimestampDecorator(LevelFilterDecorator(FileDecorator(ConsoleLogger()))) work correctly.

  3. 03
    medium

    Implement an undo/redo system for a simple spreadsheet using the Command pattern and Memento pattern together. The spreadsheet has cells that can be set to values. Commands are CellSetCommand(row, col, value). Support execute(), undo(), and redo(). Analyze the trade-off: when is Command-based undo better than Memento-based undo? Write up your analysis as code comments.

  4. 04
    hard

    Refactor the following code toward the State pattern: a TCP connection class with a connect(), send(data), and disconnect() method, where the valid operations and behaviors depend on whether the connection is CLOSED, LISTENING, ESTABLISHED, or FIN_WAIT. The current implementation uses a large if-elif chain in every method. After refactoring, adding a new state should require zero changes to the TCPConnection class.

  5. 05
    hard

    Implement the Composite pattern for a UI widget hierarchy: widgets can be Leaf (Label, Button, TextInput) or Container (Panel, ScrollView). All widgets have render(indent=0) and dimensions() methods that return (width, height). Containers compute their size from their children. Then add a Visitor that computes the total number of interactive widgets (Button and TextInput count, Label and Container do not) without modifying any widget class. Reflect on why adding a new operation is easy with Visitor but adding a new widget type requires changing all existing Visitors.

Resources

📕Design Patterns: Elements of Reusable Object-Oriented Software
bookpaidAddison-Wesley, 1994. Available at most university libraries and as a PDF through institutional access.

The original Gang of Four book (Gamma, Helm, Johnson, Vlissides). Dense but authoritative. Read the Intent and Motivation sections; the C++ code is secondary.

📕Refactoring: Improving the Design of Existing Code
bookpaidAddison-Wesley, 2nd ed. 2018. refactoring.com has the full catalogue of refactorings.

Martin Fowler's companion volume. Shows how to recognize smells and move toward patterns incrementally. The second edition uses JavaScript.

📕Head First Design Patterns
bookpaidO'Reilly Media, 2nd ed. 2021.

The most accessible introduction to GoF patterns, with Java examples and visual explanations. Excellent for building initial intuition before tackling the GoF book directly.

📕Dive Into Design Patterns
bookfree

Alexander Shvets. A modern, well-illustrated treatment of all 22 GoF patterns (excluding Interpreter) with real-world analogies and code in multiple languages. Free online.

✎SourceMaking — Design Patterns
articlefree

Concise, cross-language reference for all 23 GoF patterns with UML diagrams and code examples.

✎Wikipedia — Software Design Pattern
articlefree

Good for quick structural descriptions and UML diagrams of individual patterns.

🎓MIT 6.031 Software Construction — Reading 27: Patterns
coursefree

MIT's undergraduate software engineering course. The readings on design patterns and abstraction are especially clear and rigorous.

📕A Pattern Language (Christopher Alexander)
bookpaidOxford University Press, 1977.

The original inspiration for software design patterns — an architectural pattern language for towns and buildings. Worth reading to understand where the idea of 'patterns as solutions in context' came from.

Checkpoints — you can now…

  • ✓You can name the three GoF pattern families and the purpose of each, and give two examples from each family without looking them up.
  • ✓You can implement the Observer pattern from scratch — with subscribe, unsubscribe, and notify — and explain why it achieves loose coupling between publishers and subscribers.
  • ✓You can explain the difference between Strategy and State in terms of intent: who controls the transition, and what is the semantic meaning of the variation.
  • ✓You can explain why Singleton is controversial and describe the dependency injection alternative that provides the same sharing benefit without global state.
  • ✓Given a code smell (large switch on type, cross-cutting concerns, complex object construction, notification of changes), you can identify which pattern(s) would address it and sketch the refactored design.
  • ✓You can implement a three-layer Decorator stack (e.g., logging + caching + real service) and explain why this is more flexible than achieving the same result through multiple inheritance.
  • ✓You can state the five SOLID principles and connect at least three of them to specific GoF patterns that enforce them.
  • ✓You can explain the trade-off between Visitor and polymorphic dispatch in terms of what is easy to extend: new operations (Visitor wins) vs. new types (polymorphism wins).