The “design patterns” movement was an important step forward in object-oriented design. The 1994 book Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides carried it into the mainstream. Its four authors became known as the “Gang of Four”1. I refer to that book as GoF Design Patterns, and use design patterns for the concept.
GoF Design Patterns shows 23 solutions to particular classes of problems, along with one or more examples for each, typically in C++ but sometimes in Smalltalk. Many of those examples inspired the ones in this part of the book. This chapter introduces the concepts. One listing makes the point, and the chapters that follow supply the rest of the code.
Initially, you can think of a pattern as an especially clever and insightful way of solving a particular class of problems. Many people have worked out all the angles of a problem and have come up with the most general, flexible solution. You may have seen and solved something like it before, but your solution probably lacks the completeness a pattern embodies.
That completeness has a failure mode. Once you know a catalog of patterns, that catalog tempts you to treat it as a checklist, and to install patterns as proof of sophistication. A pattern earns its place only when you have the problem it solves. If nothing varies, you do not need machinery for isolating variation.
Although they’re called “design patterns,” they apply beyond design. Because a pattern translates directly into code, you might expect it to appear no earlier than low-level design. But a pattern embodies a complete idea within a program, so it can appear at the analysis phase or high-level design phase, where you are still describing what the system does rather than how to build it. It appears at every level, and you often discover that you need one only once you reach the code.
The basic concept of a pattern is also the basic concept of program design: adding a layer of abstraction. Whenever you abstract something, you isolate particular details. One of the most compelling motivations for abstraction is to separate things that change from things that stay the same. Once you find a part of your program that’s likely to change, patterns can prevent those changes from causing secondary effects throughout your code. That isolation makes the code cheaper to maintain and usually simpler to understand.
Isolation has a price. An abstraction is a bet about which details no caller will ever need, and a good abstraction does more than hide those details. It erases them. Code outside the boundary cannot recover information the boundary discards, so a caller that turns out to need an erased detail cannot work around the interface. Someone must reopen the interface. The art of design lies in guessing well about which details you can hide and which you must expose.
Erased details are also where scaling limits come from. Every abstraction discards something (a copy, an ordering, a lookup behind an attribute) that costs nothing at the size you built it. A growing system eventually reaches the size where one discarded detail dominates, and the layer that made the code simple now stands between you and the fix. This is one reason Performance tells you to measure at a realistic size rather than trust a small trial.
Often, the most difficult part of developing an elegant and cheap-to-maintain design is discovering what I call “the vector of change” (here, “vector” means a direction of change, not an array of numbers). You look for the most important thing that changes in your system, because that is where your greatest cost lies. Once you discover the vector of change, you have the focal point around which to structure your design.
You discover a vector of change. You do not predict it. Guessing at it up front often adds complexity to allow flexibility in a direction nobody uses. The second time a requirement shifts the same part of the design, you have evidence.
Design patterns isolate changes in your code. You have seen some design patterns in this book. For example, you can think of inheritance as a design pattern (albeit one the language builds in). It lets you express differences in behavior (that’s the thing that changes) in objects that all have the same interface (that’s what stays the same). Composition also qualifies as a pattern, since it lets you change, dynamically or statically, the objects that implement your class, and thus the way that class works.
Another pattern that appears in GoF Design Patterns is the Iterator. An iterator lets you hide the particular implementation of the container as you step through it. You can write generic code that operates on all the elements in a sequence without regard to how that sequence stores them. The code works with any object that produces an iterator.
A pattern arrives in stages, each more general than the last:
In Python terms: with open(...) for guaranteed
cleanup is an idiom, stage one, meaningless outside a language
that provides with. A dictionary mapping one
program’s shape names to its shape classes is a specific design,
stage two. The same dictionary is a standard design, stage
three, once each subclass registers itself as its
class statement runs, so adding a type never means
editing the factory (Factory builds both). Template Method is
a design pattern, stage four: a shape of solution you could
build in any language with polymorphism.
The stages are a history, not a ranking. Patterns are subtle and appear over time, so leave a solution at the stage it has reached rather than forcing it toward a design pattern.
The progression runs downward too. A pattern a language builds in drops back to stage one, and the programmers who arrive next learn it as syntax rather than as a design. Stepping through a container is stage one in Python and was stage four in the GoF Design Patterns examples.
A pattern is often a sign of something missing in a language. Programmers wrote the same scaffolding often enough that it acquired a name, and the pattern exists because the language leaves that scaffolding for them to write.
A language can supply that missing piece in two ways.
Sometimes a language grows the feature and the pattern dissolves
into it2. Iterator
is the clear case. It was implicit in the for loop
from the start, and Python 2.2 made it a protocol the language
calls on your behalf. More often the language had the piece all
along, and the pattern came from a language that didn’t.
Strategy and Command shrink to passing a
function, because a Python function is an object (Function Objects
shows both). A Factory
becomes a dictionary, because a class is an object too. Singleton becomes a
module, because Python imports each module once and caches
it.
Here is the whole of a Strategy in Python:
# strategy_is_a_function.py
from collections.abc import Callable
def apply(nums: list[int],
how: Callable[[list[int]], int]) -> int:
return how(nums)
print(apply([3, 1, 2], max), apply([3, 1, 2], sum))
#: 3 6The classic form declares a Strategy interface,
writes one class per algorithm, and adds a context class to hold
the chosen one. The how parameter replaces all
three.
This listing shows only the shape. Nobody designs a
Strategy class hierarchy around calling
max or sum; Function
Objects works through a case with a real motivation.
That replacement is why the chapters ahead keep asking the question Rethinking Objects posed: how much of each pattern’s machinery does Python still need, and how much of it becomes functions, data, and protocols?
GoF Design Patterns discusses 23 patterns and sorts them under three purposes, each named for the aspect that can vary:
The catalog above is GoF’s. Patterns from outside it, like the Null Object that Rethinking Objects builds, appear in the Pattern Catalog.
I’ve found the GoF Design Patterns classification too obscure, and not always helpful. Certainly, the Creational patterns are straightforward. How will you create objects? This is a normal question, and the name brings you right to that group of patterns. But I find Structural and Behavioral far less useful distinctions. I have not been able to look at a problem and say “clearly, you need a structural pattern here,” so that classification doesn’t lead me to a solution (I’ll readily admit that I may be missing something here).
Patterns often resemble each other more in their implementations than the GoF Design Patterns categories suggest, and this book groups them by that resemblance. Surrogate treats Proxy and State as one front-object structure. Function Objects treats Command, Strategy, and Chain of Responsibility as one function-passing structure. Composite and Interpreter treats both of its patterns as one recursive-data structure. When two patterns share a structure, learning one teaches you most of the other, and the remaining difference is intent.
Design principles are at least as important as design patterns, but they do a different job. A pattern is a shape of solution. A principle is a test you apply to whatever shape you chose: a claim you can hold the design up against. Most hold for any code, but Reflexivity and the Law of Demeter assume classes and objects. The list is a reference: later chapters name a few of these principles when a design turns on them, and the rest are here for your own designs.
Protocol is that design made literal: it names what
a caller needs from the object on the other side of a call, and
says nothing about what that object is. Rethinking
Objects builds on this, and Stateless
moves the same declaration into a function’s signature, where
Need names what the function requires of its
surroundings. This gives Managed Coupling its target: not the
least coupling, but coupling you can read.You can hold this handful of fundamental ideas in your head while analyzing a design.
Most of the chapters ahead take a pattern, or a family of patterns that share a structure, and ask three questions of it. What varies and what stays the same? That names the problem the pattern exists to solve. How much of the answer does Python supply on its own? That decides how much remains for you to write. What remains after you subtract Python’s share? That remainder is worth learning, and it is usually the intent rather than the structure.
A pattern that subtracts to nothing was not a mistake. It was the right answer for a language missing the piece Python has.
Part III closes with a Pattern Catalog, a name-and-intent index of the wider literature, with a link to this book’s coverage wherever it exists.