The classic Gang of Four patterns, grouped into the three families — creational, structural, behavioral — each with its intent, when to reach for it, and a diagram of how the pieces fit.
Student edition · understand, apply, verify
Understand what coupling each design pattern addresses and when simpler composition is sufficient.
What you should be able to do: Choose Strategy, Adapter, or Observer from a real dependency/lifecycle problem and test the behavioral interface.
Assumes basic programming. Define input and observable-behavior contracts before applying an algorithm or design heuristic. Pseudocode/fragments need surrounding context; the downloadable lab is complete and tested.
A design pattern is a named, reusable solution to a recurring design problem. Like code smells, the real value is shared vocabulary: once a team knows “Strategy” or “Adapter,” a whole structural idea travels in one word. The catalog below sticks to the patterns you actually meet: five creational (how objects get made), six structural (how objects are composed), and five behavioral (how objects collaborate and share responsibility).
Patterns are tools, not goals. Reach for one because it solves a real problem in front of you — applying a pattern for its own sake just adds indirection. The catalog and names come from Design Patterns by Gamma, Helm, Johnson & Vlissides (the “Gang of Four”); this is an original practical summary.
Read these definitions first, or use the linked terms as you go. The examples and limitations are local to this guide; companion pages are optional.
Strategy, Adapter, and Observer
Strategy selects behavior behind one contract; Adapter translates interfaces; Observer notifies subscribers. They solve different coupling problems.
Example. Inject a pricing function, wrap a legacy API, or notify subscribers about a local change.
Boundary. Specify error translation, units, subscriber reentrancy/lifetime, and unsubscribe behavior. Pattern names do not prove correctness.
Liskov substitution and interfaces
An implementation preserves its interface's behavioral expectations, not only method names. Avoid requiring more or promising less than callers expect.
Example. A read-only collection cannot pretend append always succeeds under a mutable-collection contract.
Boundary. Include errors, side effects, ownership, and state/history guarantees. Matching types alone does not establish substitutability.
Refactoring and code smells
Refactoring changes internal structure while preserving required behavior. A smell is a heuristic pointing to possible maintainability trouble, not proof of a bug.
Example. Extract repeated validation into a named function and test valid outputs and invalid-input exceptions.
Boundary. Do not mechanically split cohesive logic or add indirection. Define the maintainability problem and regression evidence first.
Aliasing and frame conditions
Aliasing means multiple names refer to one mutable object. A frame condition specifies state the operation leaves unchanged.
Example. A sorted-copy function promises it does not mutate its input. Two swap parameters may unexpectedly name one location.
Boundary. State ownership, shared references, mutation, and exception behavior. Local-variable reasoning alone does not protect aliased global state.
Preconditions and postconditions
A precondition states required inputs/state before an operation. A postcondition states the result promised when those requirements hold and execution terminates normally. Together they make requirements testable.
Example. For sum_first(b,n), require integer 0≤n≤len(b) and numeric elements; promise the sum of b[0:n].
Boundary. State exceptions, mutation, numeric domains, and empty cases. A mathematical array range does not automatically validate a runtime input.
1. Creational Patterns
Creational patterns are about how objects are made — hiding the concrete classes behind creation so client code depends on interfaces, not constructors.
1.1 Singleton
Intent: ensure a class has exactly one instance and give a single global access point to it. Use when: exactly one object must coordinate a resource — a config registry, a connection pool, a logger. Watch out: it is global mutable state — it hides dependencies and hurts testability. Prefer passing the instance in (dependency injection) when you can.
Every caller goes through getInstance() and receives the same object.
1.2 Simple Factory
Intent: put object creation behind one method that chooses and returns the right concrete product from a parameter, so callers never use new on concretes directly. Use when: creation is simple and you want it in a single place. (Strictly an idiom, not a GoF pattern — but the natural stepping stone to the next two.)
The factory owns the if type == … choice; clients depend only on Product.
1.3 Factory Method
Intent: define an interface for creating an object, but let subclasses decide which class to instantiate — the creation decision is deferred to derived classes via an overridable method. Use when: a class cannot anticipate the concrete type it must create, or wants its subclasses to specify it.
The base algorithm calls factoryMethod(); each subclass returns its own product type.
1.4 Abstract Factory
Intent: provide an interface for creating whole families of related objects without naming their concrete classes. Use when: you must choose among consistent families (e.g. a UI toolkit’s Windows vs macOS widgets) and keep every product in a family compatible.
Swap the factory and an entire, internally-consistent product family swaps with it.
1.5 Builder
Intent: separate the construction of a complex object from its representation, assembling it step by step. Use when: an object has many optional parts or a multi-step assembly — it replaces telescoping constructors and half-built objects.
The director says what steps run; the concrete builder decides how and yields the finished product.
2. Structural Patterns
Structural patterns are about how objects are composed into larger structures while keeping them flexible — mostly through wrapping and delegation rather than inheritance.
2.1 Adapter
Intent: convert the interface of an existing class into the one a client expects, letting otherwise-incompatible classes work together — usually through composition (the adapter wraps the adaptee). Use when: you must reuse a class whose interface does not match what your code needs.
The adapter speaks Target to the client and translates each call to the adaptee’s existing API.
2.2 Facade
Intent: provide one simplified, high-level interface to a complex subsystem — in effect, an adapter for a whole system. Use when: you want to hide subsystem complexity and give clients a single, easy entry point.
One friendly method fans out to the many subsystem calls the client would otherwise juggle.
2.3 Decorator
Intent: attach additional responsibilities or attributes to an object dynamically by wrapping it — a flexible alternative to subclassing. Use when: you need to add behavior to individual objects (not the whole class) and want to stack those additions freely.
A decorator is a Component and has a Component, so wrappers nest: Logging(Compressing(stream)).
2.4 Bridge
Intent: decouple an abstraction from its implementation so the two can vary independently. Use when: both an abstraction and its implementation have several variants and you want to avoid a combinatorial explosion of subclasses (e.g. Shape × Renderer).
The abstraction delegates the work across the “bridge” to an implementor; each side grows on its own.
2.5 Composite
Intent: compose objects into tree structures and let clients treat individual objects (leaves) and groups (composites) through the same interface. Use when: you have part-whole hierarchies — files and folders, UI element trees, nested menus.
A composite holds children that are themselves Components, so the tree is uniform all the way down.
2.6 Proxy
Intent: provide a surrogate or placeholder for another object to control access to it. Use when: you need lazy loading (virtual proxy), access control (protection proxy), caching, or a local stand-in for a remote object.
Same interface as the real object; the proxy decides whether, when, and how to forward each call.
3. Behavioral Patterns
Behavioral patterns are about how objects collaborate — how responsibility and algorithms are assigned and how messages flow between them.
3.1 Strategy
Intent: define a family of interchangeable algorithms, encapsulate each behind a common interface, and make them swappable at runtime — abstracting the algorithm from the code that uses it. Use when: you have several variants of a behavior and want to pick one without a sprawl of conditionals.
The context calls strategy.execute(); which algorithm runs is just which object was plugged in.
3.2 Observer
Intent: define a one-to-many dependency so that when one object changes state, all its dependents are notified automatically — publish/subscribe, a list of delegates/callbacks. Use when: many objects must react to another’s change without the subject knowing their concrete types.
Observers subscribe to the subject; on a change the subject walks its list and calls update() on each.
3.3 Template Method
Intent: define the skeleton of an algorithm in one method, deferring specific steps to subclasses. Use when: several algorithms share the same overall structure but differ in a few steps — the skeleton stays fixed, the steps vary.
The base class owns the ordering; subclasses fill in only the steps that differ (inverting control — “don’t call us, we’ll call you”).
3.4 Command
Intent: encapsulate a request as an object, so you can parameterize, queue, log, and undo operations. Use when: you need undo/redo, task queues, or to decouple the object that invokes an operation from the one that performs it.
The invoker just calls command.execute(); the command knows which receiver to act on and how to reverse it.
3.5 State
Intent: let an object alter its behavior when its internal state changes — it appears to change class. Use when: behavior depends heavily on state and you would otherwise write large conditionals branching on a state field. (Structurally like Strategy, but the states drive the transitions between themselves.)
The context forwards calls to its current state object, which may swap the context to a different state.
Choosing between them
Several patterns look alike on a class diagram; what separates them is intent. A few pairs worth keeping straight:
If you are…
Reach for
Not
Choosing an algorithm at runtime
Strategy
State (which also transitions itself)
Changing behavior as internal state changes
State
Strategy (client picks; it doesn’t transition)
Making a mismatched interface fit
Adapter
Facade (simplifies, not translates)
Simplifying a whole subsystem
Facade
Adapter (one class, not a system)
Adding behavior by wrapping, same interface
Decorator
Proxy (controls access, doesn’t add features)
Controlling access / lazy-loading, same interface
Proxy
Decorator (enhances behavior)
Deferring which class to instantiate to a subclass
Factory Method
Abstract Factory (whole families)
Creating consistent families of products
Abstract Factory
Factory Method (single product)
Decoupling two dimensions that each vary
Bridge
Adapter (fixes an existing mismatch)
The recurring theme: creational patterns hide how objects are made, structural patterns compose objects through wrapping and delegation, and behavioral patterns assign responsibility and let objects talk. Learn the intent first — the class diagram is just how that intent is wired.
Worked example: swap a pricing strategy safely
Use this small example to connect the concept to a concrete decision.
Sequence for this example. The table explains the evidence and decision at each step.
Step
Evidence, decision, and expected result
Specify a pricing contract
Given nonnegative integer cents, return nonnegative integer cents without mutating inputs.
Inject behavior
Use a function/object implementing the contract. A simple callable can be enough; a class hierarchy is not mandatory.
Adapt a legacy service
Translate its units and errors explicitly at the boundary. Matching method names is not sufficient.
Notify subscribers carefully
Snapshot subscribers or define mutation rules during notification. Decide how unsubscribe/reentrancy and one handler's error affect the rest.
Check your understanding: Does naming a design pattern make the design correct?
No. Contracts, lifecycle, errors, and state ownership still need reasoning and tests. Patterns address specific forces, not every problem.
Run and change the example
Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:
python3 guide_labs.py patterns
python3 guide_labs.py test
The lab uses callable strategies and a small adapter/observer with stated mutation and error behavior.
The program tests its stated finite contracts. It is an educational model, not an implementation of the named production product or a proof of unmodeled distributed faults.