A design pattern is a named, reusable solution to a recurring design problem. The Gang of Four book catalogued twenty-three in 1994, but the value today is not the catalog — it is the shared vocabulary that lets one engineer say “this is a Strategy” and another immediately picture the shape of the code.
Patterns become dogmatic only when applied without context. The modern view: learn the few that show up in every codebase, recognise their costs, and reach for them when the problem genuinely fits — never as a default.
The Four You Meet Everywhere
| Pattern | Problem it solves | Modern shape |
|---|---|---|
| Strategy | Pick an algorithm at runtime without if/else ladders | A function passed as an argument; a “callable” interface |
| Observer | Notify N subscribers when one thing changes | Event emitters, reactive streams, subscribe callbacks |
| Decorator | Wrap an object to add behaviour without subclassing | Middleware chains (Express, Rack), decorator syntax (Python) |
| Factory | Decouple what gets built from how it gets built | A builder function; dependency-injection containers |
Four cases cover ninety percent of pattern references in production code. Internalise these and most “pattern” conversations follow; the remaining GoF entries (Visitor, Command, Iterator) are useful but rarer.
Composition vs Inheritance
The single principle that has displaced the most pattern usage is composition over inheritance. A class that has-a helper can swap that helper at runtime; a class that is-a parent is welded to its parent’s interface for life.
Inheritance Composition
class Duck extends Bird class Duck { flight: Flyer; quack: Quacker }
— flight baked in — flight can be swapped (FlyWithWings → NoFly)
— quack built-in — quack can be swapped independently
Inheritance remains the right tool when subclasses truly substitute for the parent (the Liskov rule). Composition wins whenever behaviour is orthogonal to type.
Functional Alternatives
Several “classic” patterns shrink to a single function in a language with first-class functions:
| Classic pattern | Functional replacement |
|---|---|
| Strategy | Pass a higher-order function |
| Template Method | Pass callbacks into a generic function |
| Command | An object literal { run, undo } — or just the function |
| Iterator | Any iterable / generator (yield) |
| Observer | A list of callbacks; subscribe adds one |
If the language supports closures and generics, reaching for an interface-and-class Strategy is a tell that the pattern was cargo-culted. The goal is the behaviour, not the shape — closures are the cheaper shape.
Anti-Patterns to Avoid
- God Object: one class “to keep things simple” that accumulates 50+ methods. Split by responsibility; the names get clearer.
- Premature Singletons: single-instance state becomes a global — invisible coupling, untestable, every test inherits every other test’s state.
- Pattern by Name Only: a “Factory” that has one product, a “Strategy” with one strategy, an “Observer” with zero subscribers. Each was an excuse to use a pattern; each should be deleted.
- Russian-Doll Inheritance:
A → B → C → D → E, where changingBis a minefield. Composition flattens the chain.
When Patterns Hurt
A pattern is right when the third instance of the problem appears — not the first. The rule of three:
- First occurrence: write the inline code. YAGNI (You Aren’t Gonna Need It) applies.
- Second occurrence: copy-paste is faster than the wrong abstraction.
- Third occurrence: now the shape is real; the pattern (or its functional equivalent) is justified.
Premature abstraction is more expensive than duplication, because abstractions ossify: once three callers depend on the wrong shape, every refactor must satisfy all three.
Practice Trajectory
- Pick any codebase you know; find one Strategy and one Observer. Describe the problem each solves in one sentence.
- Refactor a 4-branch
if/elsechain into aMap<string, Strategy>and observe how the type system now documents the cases. - Replace a class-based Strategy with a higher-order function and note what becomes simpler (no interface, no ceremony) and what is lost (no place to put a doc-comment interface).
- Audit a codebase for “patterns by name only” — factories with one product, singletons with no shared state, abstract classes with one concrete subclass — and delete one.
- Choose a GoF entry you have never used (Visitor, Memento, Chain of Responsibility) and write one paragraph: what problem it solves, when it’s the wrong tool, and a modern alternative.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Three or more cases of the same shape | A pattern (or its functional form) is now justified |
| Reviewing unfamiliar code | Pattern names are the dictionary; learn them for reading, not for writing |
| Choosing a language | Native closure/generics let you express most patterns as plain code |
| Designing a library API | Prefer composition + a small interface; reserve inheritance for true subtypes |
A pattern is a tool to communicate and structure, never a checklist for promotion.