GoF patterns
The Design Patterns catalogue is still the common vocabulary of object-oriented design. It is also thirty years old, written against languages without first-class functions, and parts of it have been absorbed into the languages themselves.
Both facts matter. Teaching the catalogue uncritically produces developers who write a Strategy where a lambda would do; ignoring it produces developers who cannot name what they are looking at.
The three families
Section titled “The three families”Creational — how objects come to exist. Factory Method, Abstract Factory, Builder, Prototype, Singleton.
Structural — how objects compose. Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy.
Behavioural — how objects collaborate. Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor.
Which still earn their keep
Section titled “Which still earn their keep”- Adapter and Facade — the everyday patterns of any boundary. An anti-corruption layer is an Adapter with a domain motive.
- Composite — whenever a tree has to be treated uniformly.
- Decorator — the pattern behind most middleware, and the reason it composes.
- Builder — where a constructor has too many parameters and half of them are optional. Its real value is making an invalid object unconstructable.
- State and Strategy — still the right answer when the variation is real and named. Not when it is one branch.
Which the language absorbed
Section titled “Which the language absorbed”Iterator is a language feature everywhere now. Command and much of Strategy are a function value in any language with first-class functions. Template Method is usually better expressed as composition — the inheritance it requires costs more than the reuse it provides.
Recognising these is not pedantry: implementing a lambda as a five-class Strategy hierarchy is exactly the over-application this catalogue is prone to.
Singleton
Section titled “Singleton”Kept separate because it is the one that is usually a mistake. It couples every caller to a global, it makes the dependency invisible in the signature, and it makes the code untestable in parallel. What people want from it is one instance, which is a lifecycle question their dependency injection container already answers.
Worked example — Strategy, and when it is a lambda
Section titled “Worked example — Strategy, and when it is a lambda”The over-application looks like this. Five files, one interface, one factory, and
a switch that has merely moved:
public interface DiscountStrategy { Money apply(Money total); }
public class NoDiscount implements DiscountStrategy { /* ... */ }public class PercentageDiscount implements DiscountStrategy { /* ... */ }public class FixedDiscount implements DiscountStrategy { /* ... */ }
public class DiscountStrategyFactory { public DiscountStrategy forCode(String code) { switch (code) { case "NONE": return new NoDiscount(); case "PCT": return new PercentageDiscount(); case "FIX": return new FixedDiscount(); default: throw new IllegalArgumentException(code); } }}When the variation is a single expression, it is a function value, and the catalogue’s own advice — encapsulate what varies — is fully satisfied by one line:
Function<Money, Money> tenPercentOff = total -> total.minus(total.percentage(10));And when the variation is real — several implementations, each with its own
state, tested separately, named by the business — the five files were right all
along. The deciding question is not “is Strategy a good pattern”, it is how
many implementations will exist, and does each one deserve a name a domain expert
would recognise. If it does, you are usually looking at a
specification or a policy rather than
a Strategy, and naming it that way says more.
Worked example — Builder, for the reason people forget
Section titled “Worked example — Builder, for the reason people forget”Builder is normally taught as a cure for long parameter lists. Its real value is that it can refuse to build:
public final class Shipment { private Shipment(Address destination, Carrier carrier, Instant pickup) { /* ... */ }
public static Builder to(Address destination) { return new Builder(destination); }
public static final class Builder { // ... destination captured, carrier and pickup optional-but-paired
public Shipment build() { if (carrier == null ^ pickup == null) throw new IllegalStateException("a carrier and a pickup time go together"); return new Shipment(destination, carrier, pickup); } }}A constructor with seven nullable parameters can express that rule only in a comment. This is also the point where a builder and a factory stop being distinguishable and it stops mattering: both exist so that nothing downstream has to check.
How to use this catalogue
Section titled “How to use this catalogue”Read it to name what you are looking at, not to decide what to build. The question is never “which pattern should I use here” — it is “what varies, what stays, and what does the code need to say to the next person who reads it”.
Status
Section titled “Status”Outline, with the two entries above worked through because they are the two most often over-applied. The remaining per-pattern pages, each with a rendering per stack, are not written yet.