In a language with first-class functions, when is a plain function or lambda a sufficient implementation of the Strategy pattern, and when do you still want a named interface with classes?
answer
- lambda = object with one method
- closure captures the config
- promote when you need a name/lookup
- multi-op family → named type
- SAM interface = both worlds
basics
~20 sIf the strategy is one operation with no state and no extra methods, a function value is the whole pattern — a lambda is an object with one method. Prefer a named interface or class when the strategy needs configuration, identity, a name for DI/registry lookup, or more than one operation.
solid answer
~50 sStrategy's essence is "pass in behavior"; a first-class function *is* behavior as a value, so `sort(list, byLastName)` is textbook Strategy with zero boilerplate. Use a bare function when: the contract is one operation; the variant is stateless or captures its config in a closure; and callers don't need to identify or introspect it. Reach for a named type when you need (a) more than one operation in the family, (b) a discoverable name — DI registration, a `Map<code, Strategy>` registry, config- or annotation-driven lookup, logging "which policy ran"; (c) per-strategy configuration and validation better expressed as constructor parameters; (d) extra metadata such as `supports(input)` for chain-of-responsibility-style selection, priority, or feature-flag keys; (e) equality/serialization of the chosen policy. A good middle ground in many languages is a **named single-method interface** that lambdas can still satisfy: you get the documented contract and the name at the call site, plus lambda-lightness at the definition site.
code
typescript · 13 lines// enough as a function: one op, config in a closure
type Backoff = (attempt: number) => number;
const exponential = (baseMs: number): Backoff => a => baseMs * 2 ** a;
retry(call, exponential(100));
// promote to a named type once you need lookup + identity + metadata
interface PaymentStrategy {
readonly code: string; // persisted, logged, looked up
supports(o: Order): boolean; // self-selection
charge(o: Order): Receipt;
}
const registry = new Map<string, PaymentStrategy>();
const chosen = [...registry.values()].find(s => s.supports(order));go deeper
Say a lambda is behavior as a value, so passing a comparator to a sort function is Strategy; classes are optional.
Give the promotion criteria: multiple operations, DI/registry lookup by key, per-strategy configuration, and the need to name the policy in logs.
Discuss structural-vs-nominal typing hazards, single-abstract-method interfaces as the hybrid, partial application for configuration, and closure-captured mutable state as a thread-safety trap.
Frame it as extension-point design: a named contract is a published API with discoverability, versioning, and observability guarantees; a bare function type is an anonymous, unversioned seam other teams cannot find.
## The core observation Strategy exists because older object-oriented languages had no way to pass *behavior* as a value — the only container for behavior was an object, so you wrapped one method in a class. In a language with **first-class functions** (functions can be stored in variables, passed as arguments, returned, and can capture surrounding variables in a **closure**), that wrapper becomes optional. A lambda is, semantically, an object with exactly one method. That is why every language ships strategies without calling them that: comparator/key functions for sorting, predicates for filtering, callbacks for retry decisions, hash functions, reducers, middleware. ``` // Strategy, no classes, no ceremony sort(people, by = person -> person.lastName) type Retry = (attempt: int, error: Error) -> Duration | STOP httpClient(retry = (n, e) -> n < 3 ? seconds(2^n) : STOP) // config captured in a closure ``` ## When the function form is enough - **One operation.** The family has a single method. Two or more related operations mean the interface carries real structure, and a bundle of loose lambdas becomes hard to keep coherent. - **Stateless, or state captured by closure.** A closure can hold configuration (`perKgRate`) as neatly as a constructor field. - **No identity needed.** Nobody must ask "which one is this?" — you don't log it, persist it, compare it, or look it up by key. - **Locally created.** The variants are defined near the call site, so the loss of a self-documenting type name costs little. ## When to keep a named type 1. **Multiple operations.** `encode`/`decode`, `serialize`/`deserialize`, `apply`/`undo` belong together; a pair of independent lambdas can be mismatched at wiring time, while one type makes the pairing structural. 2. **Registry or DI lookup.** Frameworks resolve by *type* or by *bean name*. A `Map<PaymentMethod, PaymentStrategy>` populated by scanning implementations is a common, robust selection mechanism; anonymous lambdas can't be discovered that way. 3. **Rich configuration and validation.** A constructor that rejects a negative rate at wiring time is better than a closure that fails on first use. 4. **Metadata alongside the behavior.** Self-selecting strategies (`supports(context): boolean`), ordering/priority, a stable code persisted in a database, human-readable descriptions for an admin UI, or capability flags all need a place to live. 5. **Observability and debugging.** A stack frame that reads `ExpressShipping.cost` beats `lambda$total$3`. Logging "pricing policy = TIERED_V2" requires a name. 6. **Documentation and discoverability.** A named interface tells the next developer that an extension point exists, and "find implementations" enumerates the family. A `(Order) -> Money` parameter tells them almost nothing about intent, and two unrelated concepts with the same shape become interchangeable by accident — a **structural-typing** hazard. Naming the type restores the semantic distinction. 7. **Serialization / equality.** If the chosen policy is part of persisted state or of a value object's equality, functions are unhelpful (comparing or serializing closures is impractical). ## The hybrid Many languages let a lambda implement a single-abstract-method interface directly. That is usually the best of both: a documented, named, DI-discoverable contract *and* a one-line definition. ``` interface PricingPolicy { price(order): Money } // named, documented, injectable register("FLAT", order -> flatRate) // still a lambda register("WEIGHT", order -> order.weightKg * perKg) ``` Another useful hybrid is a **partially applied function**: a factory function that takes configuration and returns a closure (`weightBased(perKg = 1.2)`), giving constructor-style configuration without a class. ## Pitfalls of the function form - **Parameter-position ambiguity**: `process(items, f, g)` where both are `(T) -> T` is easy to call wrong; named types or named arguments fix it. - **Overly generic signatures** invite passing an unrelated function with a matching shape. - **Testing**: still trivial — a strategy function is a pure function, arguably easier to test than a class. - **Mutable captured state**: a closure capturing a mutable variable makes a supposedly interchangeable algorithm stateful and non-thread-safe; the same hazard exists for class fields, but closures hide it better. - **Hot paths**: a per-call allocated closure can add pressure that a shared singleton strategy object avoids; hoist the value rather than reverting to classes. ## Rule of thumb Start with a function value. Promote it to a named type the first time you need to *name*, *look up*, *configure*, *pair*, or *observe* the strategy. The pattern is unchanged either way — only the syntax of "behavior as a value" differs.
- Does using a lambda mean you are no longer 'using the Strategy pattern'?No. The pattern is defined by intent — interchangeable algorithms selected by the client — not by class count. The lambda form is the same pattern with the boilerplate removed.
- How do you keep type safety when several strategies share the same function signature?Name the type (a single-method interface, or a language-level type alias that is nominal rather than structural), use named parameters, or wrap the function in a tiny value type so an unrelated function with the same shape cannot be passed by accident.
- How would you unit-test a strategy expressed as a closure?Call it directly with crafted inputs and assert the output — it is a pure function. Test the *selection* logic separately: assert the factory or registry returns the expected strategy for each key or condition.
A recipe card versus a hired chef. For "whisk it 30 seconds" a card (lambda) is plenty. When the job needs equipment, a name on the roster, a schedule, and someone to blame in the logs, you hire a chef (a named class).
saying these in an interview costs you the question
- "Strategy requires classes" — behavior-as-a-value is the essence; classes are one encoding of it.
- Wrapping every lambda in a class 'because the pattern says so', producing files that add no information.
- Passing several same-shaped lambdas positionally and letting the compiler bless a mix-up.
- Capturing mutable state in a strategy closure and then sharing it across threads.
- Assuming a lambda strategy can be discovered by a DI container or a registry scan — it cannot without registration.