Skip to main content

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:

  1. One method? If yes, prefer a function type. If no, use an object.
  2. State or lifecycle? If the variant owns resources or needs close(), use an object.
  3. Configured at construction? If yes, and that configuration is reused, use an object. If configuration is per call, use a closure that captures it.
  4. Testing? Both are trivial to test. Do not let test convenience pick for you.
  5. 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

  1. What three properties push you toward a Strategy object over a function?
  2. Why is a one-method interface with only one implementation a smell?
  3. When is "just a function" still worth naming and extracting?
  4. 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.

  1. Rewrite it to accept a list of (value) -> boolean functions.
  2. Identify which validators actually have internal state (regex cache, DB connection, counter) and keep them as objects.
  3. Document why each choice was made in a one-line comment above the validator.

Stop when the code reads cleanly at both call sites.

Read This Only If Stuck