Which classic Gang of Four patterns collapse into a one-liner in a language with first-class functions, and what exactly is lost or kept when they do?
answer
- one-method interface = a function in a trench coat
- Strategy/Command/Factory/Template → lambdas
- Visitor → pattern matching + sum types
- lose: name, identity, multi-method, serialisation, discoverability
- intent survives, ceremony dies
basics
~20 sStrategy, Command, Template Method hooks, simple Factories and much of Observer collapse: instead of an interface plus an implementing class, you pass a function or lambda. The intent survives — varying behaviour is still injected — but the ceremony (extra types, boilerplate) disappears.
solid answer
~60 sPatterns whose interface has a single method are really 'a function in a trench coat', so a language with first-class functions expresses them directly. Strategy becomes passing a function; Command becomes a closure capturing its arguments; Template Method's variable steps become function parameters (avoiding inheritance); Factory Method becomes passing a constructor reference or supplier; Observer becomes a list of callbacks or a built-in event/stream type; Iterator becomes generators or built-in lazy sequences; Decorator over a single operation becomes function composition. What you keep is the intent and the varying axis — you have still separated what varies from what stays. What you lose is sometimes worth restoring: a named type documents intent and constrains the contract, supports multiple related methods, carries state and identity (needed for unsubscribing an Observer, or for equality/serialisation of a Command), enables exhaustive discovery of implementations, and gives better stack traces and debuggability. The rule of thumb: one method and no identity requirement → use a function; multiple methods, meaningful identity, or a name that documents a domain concept → keep the type.
code
pseudocode · 10 lines// Classic Strategy: interface + class per algorithm
interface DiscountStrategy { Money apply(Order o); }
class SeasonalDiscount implements DiscountStrategy { Money apply(Order o) { ... } }
checkout(order, new SeasonalDiscount());
// With first-class functions: the strategy IS the function
checkout(order, o -> o.total() * 0.9);
// Take the type back when it earns its keep (name + second method + identity):
type DiscountPolicy = { name: String, apply: (Order) -> Money, explain: (Order) -> String }go deeper
Name two: Strategy and Command become 'just pass a function/lambda', and say the intent (varying behaviour) is unchanged.
Cover several patterns (Strategy, Command, Template Method, Factory, Iterator, Observer) and note that a one-method interface is equivalent to a function type.
Argue both directions: what collapses, and the concrete reasons to keep a named type — multi-method contracts, identity for unsubscribe, serialisation for replay, discoverability, domain naming.
Reframe patterns as vocabulary rather than code templates, discuss how language evolution absorbs patterns (Visitor → pattern matching, Singleton → DI scope), and set team guidance on when a lambda is enough versus when a type must exist.
## Why this happens at all Many GoF patterns were catalogued in the early 1990s against languages (C++, Smalltalk, later Java) where **behaviour could not be passed around as a value**. If you wanted to hand a caller 'a chunk of behaviour', your only vehicle was an object implementing an interface. So the catalogue is full of patterns whose interface has exactly **one method** — and a one-method interface is functionally identical to a function type. Once a language has **first-class functions** (functions storable in variables, passed as arguments, returned from functions) plus **closures** (functions that capture variables from their defining scope), those patterns lose their scaffolding, not their purpose. ## Pattern by pattern **Strategy** — interchangeable algorithms behind one interface. With functions: pass the function. `sort(items, byPriceDescending)`. The varying axis is unchanged; only the `interface Comparator { int compare(a,b) }` + implementing class disappear. **Command** — encapsulate a request as an object so it can be queued, logged, undone. A closure that captures its arguments *is* a deferred request: `queue.add(() -> transfer(from, to, amount))`. Kept when you need **undo** (two operations: execute/undo → two methods → keep a type) or **serialisation/inspection** (a lambda is opaque; a data object carries fields you can persist, replay or audit). **Template Method** — a fixed skeleton with overridable steps, classically via inheritance. With functions, the steps become parameters: `runReport(fetch = ..., format = ...)`. This also converts inheritance coupling into composition, which is usually a win (no fragile base class, steps are testable alone, several variations combinable). **Factory Method / Abstract Factory (simple cases)** — becomes passing a constructor reference or a supplier function `() -> T`. Abstract Factory with several related products still benefits from a type, because it groups multiple creation methods that must stay consistent. **Observer** — becomes a list of callbacks, or a built-in language/library facility: events, signals, reactive streams, channels. The subtlety: **unsubscription needs identity**. A lambda has no stable identity you can reliably remove later, so APIs return a subscription/disposable handle instead — which is itself a small object, i.e. the type came back where it was actually needed. **Iterator** — dissolves into generators (`yield`), coroutines or built-in lazy sequences; writing a manual `hasNext/next` class in such a language is unidiomatic. **Decorator** over a single operation — becomes function composition or higher-order wrapping: `withRetry(withLogging(call))`. Over a multi-method interface it stays a class (you must forward every method), which is why languages with delegation syntax make it cheaper. **Visitor** — largely replaced by **pattern matching / sum types** in languages that have them; the double dispatch it simulated is provided directly, with exhaustiveness checking by the compiler. **Singleton** — mostly replaced by module-level values, language `object` declarations, or a DI container's lifetime scope — which also restores testability that classic Singleton destroyed. **Null Object** — largely replaced by option/maybe types and null-safe operators. ## What survives the collapse (and why patterns still matter) The **intent, forces and consequences** survive. Saying "we injected a pricing *strategy*" still communicates *why* the parameter exists, even when the parameter is a lambda. Pattern names remain design vocabulary; only the mandatory class scaffolding goes. ## What you genuinely lose with a bare function — and when to take the type back 1. **Documented intent / domain name.** `Function<Order, Money>` says nothing; `PricingPolicy` says a lot. A named single-method interface (or type alias) restores this at near-zero cost. 2. **Multiple related operations.** Two or more methods that must vary together (execute/undo, encode/decode, serialize/deserialize) belong in one type, not two loose functions that can drift apart. 3. **Identity and lifecycle.** Unsubscribe, deduplicate, compare, cache-key, persist — all need something with identity. Lambdas generally do not offer stable equality. 4. **State.** A closure *can* hold state, but hidden captured state is harder to inspect, reset or test than an explicit object field. 5. **Discoverability.** "Find all implementations of `PricingPolicy`" is a one-keystroke IDE query; "find every lambda ever passed to this parameter" is not. 6. **Serialisation, replay, audit.** Event-sourced or queued commands must survive a restart — they must be data, not code. 7. **Diagnostics.** Stack frames for anonymous lambdas are less informative; deep chains of composed closures can be hard to debug. 8. **Constraints beyond the signature.** A type can carry documentation, validation in its constructor, and invariants; a raw function type cannot. ## Trade-off framing for an interview > A one-method interface with no identity requirement is a function; express it as one. Keep a type when it earns its keep: multiple co-varying methods, identity/lifecycle, state you must inspect, serialisation, or a domain name that makes the code self-documenting. Either way, the *pattern* is still the right word for the design decision — what changed is the amount of code it costs. ## Common over-corrections - **"Patterns are dead in functional languages."** No — the *problems* recur; some solutions become built-in. Functional languages have their own pattern vocabulary too. - **Lambda soup:** replacing every type with an untyped callback until no signature communicates anything and errors surface only at runtime. - **Cargo-culting the class version** into a language with first-class functions because a book from 1994 drew a UML diagram that way.
- When would you still write Command as a class rather than a closure?When the request must be data: persisted to a queue or event store, replayed after restart, audited, deduplicated by identity, or undone (execute + undo are two co-varying operations, which is a type, not a function).
- Why does replacing Observer callbacks with lambdas complicate unsubscription?Removal needs a stable identity to match against, and lambdas generally have no reliable equality — two structurally identical lambdas are distinct objects. Well-designed APIs therefore return a subscription/disposable handle from subscribe, and you call close on that.
- Does the same argument dissolve Adapter or Composite?Much less. Adapter usually bridges multi-method interfaces, and Composite is about a recursive tree of uniformly-treated parts. Both need real types; only their single-operation degenerate cases reduce to functions.
saying these in an interview costs you the question
- 'Design patterns are obsolete in modern languages' — the problems persist; only some implementations became syntax.
- Replacing every named interface with a raw function type, destroying intent and type safety.
- Claiming lambdas can always replace Command, ignoring serialisation, undo and audit requirements.
- Assuming Template Method must use inheritance, when passing step functions is usually the better composition.
- Not knowing that unsubscribing a lambda observer requires a handle because lambdas lack stable identity.