Design Patterns as Shared Vocabulary — and as Symptoms

Which of the GoF patterns became language features, which are still real design decisions, and how to read a pattern as a symptom — a workaround for something the language or the architecture is missing.

The Gang of Four catalogued 23 patterns in 1994, and the book's lasting contribution turned out to be the names, not the implementations. "Wrap it in a decorator" communicates a structure in three words to someone who has never seen the code. That is worth a great deal. What is not worth anything is treating the catalogue as a menu of things a good codebase should contain — which is how it is usually taught, and which produces the AbstractRequestHandlerFactoryBean genre of software.

The productive way to read a pattern is as a response to a constraint. Change the constraint — a language gains first-class functions, or the architecture gains a message bus — and the pattern either becomes invisible or becomes unnecessary.

Patterns that are now language features #

Norvig made this argument in 1996: many GoF patterns exist to work around what C++ could not express, and in a language with first-class functions, closures and dynamic dispatch they either disappear into the language or shrink to a line.

Pattern What it is today
Strategy a function parameter — sort(list, comparator), retry(policy)
Command a closure, or a Runnable/Func value
Template Method a higher-order function taking the varying step
Iterator built into the language (for…in, IEnumerable, Iterator)
Observer built-in events, streams, Flow, signals (events)
Singleton a module, a Kotlin object, a Rust OnceLock, a DI container's scope
Factory Method a static factory function, or generics + a constructor reference
Prototype copy(), with, structural sharing (immutability)
java
// Strategy, GoF-style: an interface, an implementation per behaviour, a field, wiring
interface DiscountStrategy { Money apply(Money m); }
class SeasonalDiscount implements DiscountStrategy { … }

// Strategy, today
Money total = applyDiscount(subtotal, m -> m.times(0.9));

When the strategy has state, a name worth having, or several methods, the interface earns its place again. The point is that the pattern was never the interface — it was "make the varying part a parameter".

Patterns that are still real design decisions #

These describe structures the language does not give you, and knowing them is knowing your options.

  • Adapter — make an existing type satisfy an interface it was not built for. The honest tool for integrating a third-party library without letting its types spread through your code.
  • Decorator — add behaviour by wrapping, preserving the interface. The composition-based answer to subclassing, and the reason BufferedInputStream(FileInputStream(…)) composes at all (inheritance vs composition).
  • Facade — one small interface over a complicated subsystem. This is just Ousterhout's deep module with a name, and it is the pattern most worth applying deliberately.
  • Proxy — same interface, intercepted calls: lazy loading, caching, remoting, access control. Every ORM's lazy collection and every RPC client stub is one.
  • Builder — staged construction of an object that must be valid the instant it exists (object lifecycle).
  • State — when an object's behaviour genuinely changes with its lifecycle, replacing a switch (status) repeated in eight methods.
  • Visitor — operations over a closed type hierarchy; largely superseded by sealed types plus exhaustive pattern matching, which give the compiler the check that visitor gives up (dispatch).

Reading a pattern as a symptom #

When you seeOften means
Singletonglobal mutable state — untestable, order-dependent initialization, a shared-mutability hazard under concurrency. Usually wants to be an ordinary object passed in
Abstract Factory over one product familyspeculative generality; the second family never arrived
A Manager/Helper/Util classbehaviour that belongs on the data it operates on — the "ask" side of tell-don't-ask
Deep Decorator stacksflexible, but a stack trace nobody can read and a debugging cost that grows with depth
Visitor in a language with sealed typesa pattern paying for a language feature you already have
An interface with one implementation and one mocka header interface, not an abstraction — nothing varies

Where the value actually is #

  • In review and in naming. "This is a decorator, not a subclass" ends an argument in one sentence. A class called RetryingRateSource tells a reader the structure before they open it.
  • In recognizing the shape you are already building. Half of pattern knowledge is noticing that the thing you invented has a name, a known failure mode, and a known alternative.
  • Not in a checklist. No codebase is improved by containing more patterns. Ousterhout's framing is the right corrective: the measure is whether the abstraction lets callers stop thinking about something, and an unnecessary pattern is one more thing to think about.

Sources

  1. bookDesign Patterns: Elements of Reusable Object-Oriented Software
  2. bookRefactoring: Improving the Design of Existing Code, 2nd Edition
  3. talkNorvig — Design Patterns in Dynamic Languages (1996 talk, slides online 1998)
  4. bookA Philosophy of Software Design, 2nd Edition

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close