Skip to main content
Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Software Engineering Craft

Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Design Patterns & Idiomatic Refactoring

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

PatternProblem it solvesModern shape
StrategyPick an algorithm at runtime without if/else laddersA function passed as an argument; a “callable” interface
ObserverNotify N subscribers when one thing changesEvent emitters, reactive streams, subscribe callbacks
DecoratorWrap an object to add behaviour without subclassingMiddleware chains (Express, Rack), decorator syntax (Python)
FactoryDecouple what gets built from how it gets builtA 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 patternFunctional replacement
StrategyPass a higher-order function
Template MethodPass callbacks into a generic function
CommandAn object literal { run, undo } — or just the function
IteratorAny iterable / generator (yield)
ObserverA 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 changing B is 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:

  1. First occurrence: write the inline code. YAGNI (You Aren’t Gonna Need It) applies.
  2. Second occurrence: copy-paste is faster than the wrong abstraction.
  3. 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

  1. Pick any codebase you know; find one Strategy and one Observer. Describe the problem each solves in one sentence.
  2. Refactor a 4-branch if/else chain into a Map<string, Strategy> and observe how the type system now documents the cases.
  3. 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).
  4. 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.
  5. 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

SituationTakeaway
Three or more cases of the same shapeA pattern (or its functional form) is now justified
Reviewing unfamiliar codePattern names are the dictionary; learn them for reading, not for writing
Choosing a languageNative closure/generics let you express most patterns as plain code
Designing a library APIPrefer composition + a small interface; reserve inheritance for true subtypes

A pattern is a tool to communicate and structure, never a checklist for promotion.