Strategy vs Function References and Closures
What This Concept Is
A classical Strategy is an object implementing a one-method interface. In languages with first-class functions, a plain function reference or a closure often does the same job with less ceremony.
The question is not "which is better". It is: when does the OO form earn its weight, and when should you just pass a function?
Decision axis:
- Function reference / closure -- the strategy has one method, no state worth naming, no lifecycle, and no configuration beyond simple parameters.
- Strategy object -- multiple related operations, nontrivial internal state, construction-time configuration, or the strategy itself needs dependency injection.
Why It Matters Here
A lot of "Strategy" code in real projects is a single-method interface, one implementation, and no swap point. That is a closure with boilerplate on top. Learning when to drop the class is part of learning the pattern, not a rebellion against it.
Going the other direction matters too: if you keep adding parameters and flags to a "simple function", you are hand-building a Strategy object badly. Promote it.
Concrete Example
Sorting in TypeScript. The comparator is the varying step; no object is required.
type User = { name: string; lastLogin: Date; priority: number };
type Comparator<T> = (a: T, b: T) => number;
const byName: Comparator<User> =
(a, b) => a.name.localeCompare(b.name);
const byRecentLogin: Comparator<User> =
(a, b) => b.lastLogin.getTime() - a.lastLogin.getTime();
// Closure that captures configuration.
function byPriorityThen(tieBreaker: Comparator<User>): Comparator<User> {
return (a, b) => (b.priority - a.priority) || tieBreaker(a, b);
}
function sortUsers(users: User[], cmp: Comparator<User>): User[] {
return [...users].sort(cmp);
}
sortUsers(users, byName);
sortUsers(users, byPriorityThen(byRecentLogin));
Now compare against a Strategy object version:
interface UserComparator {
compare(a: User, b: User): number;
}
class ByName implements UserComparator {
compare(a: User, b: User) { return a.name.localeCompare(b.name); }
}
For a single method with no state, the interface costs two files and buys nothing. If ByName later needed a locale, a tie-breaker policy, and a logger, the class pays for itself.
Common Confusion / Misconception
- "Always use classes in OO." Modern Java, C#, Python, Kotlin, Scala, and Swift all support function types or SAM conversion. GoF itself notes that closures can replace Strategy when the strategy has no state.
- "Always use functions -- classes are ceremony." If the strategy needs named state, invariants, or multiple methods, classes are clearer. Functions with five captured variables are worse than a small object.
- Function reference is not the same as inline lambda forever. Extract the function to a name once there are two call sites. Anonymous strategies hide intent.
How To Use It
Apply this checklist when you think you want Strategy:
- One method? If yes, prefer a function type. If no, use an object.
- State or lifecycle? If the variant owns resources or needs
close(), use an object. - Configured at construction? If yes, and that configuration is reused, use an object. If configuration is per call, use a closure that captures it.
- Testing? Both are trivial to test. Do not let test convenience pick for you.
- Team conventions? In a codebase where strategies are objects everywhere, do not introduce one-off functions without discussion.
Hybrid is fine. A compare closure and a Paymentprocessor object can coexist.
Check Yourself
- What three properties push you toward a Strategy object over a function?
- Why is a one-method interface with only one implementation a smell?
- When is "just a function" still worth naming and extracting?
- Where does language idiom (Java SAM, Python callables, TS types) affect the decision?
Mini Drill or Application
Take a validation component that currently accepts a list of validator objects, each with a single validate(value): boolean method.
- Rewrite it to accept a list of
(value) -> booleanfunctions. - Identify which validators actually have internal state (regex cache, DB connection, counter) and keep them as objects.
- Document why each choice was made in a one-line comment above the validator.
Stop when the code reads cleanly at both call sites.