Skip to main content

Strategy vs Function References and Closures

What This Concept Is​

A Strategy encapsulates interchangeable behavior behind a contract that may have one or several operations. For a single operation in a language with first-class functions, a plain function reference or closure often supplies that boundary compactly.

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 -- convenient for a callable operation, including captured configuration and injected dependencies with clear ownership.
  • Strategy object -- useful for multiple related operations, named invariants or an explicit resource lifecycle.

Why It Matters Here​

An interface with one implementation can support a valuable dependency boundary, or can add ceremony without benefit. Explain the testing, ownership or variation pressure before choosing its representation.

If flags and captured state obscure the contract, compare a named configuration value, smaller functions and an object with explicit invariants. None is an automatic consequence of parameter count.

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); }
}

A function type is compact here; an interface does not inherently require separate files. Configuration and named lifecycle responsibilities can justify a class, but a configured closure can also carry a locale or logger.

Common Confusion / Misconception​

  • "Always use classes in OO." Modern Java, C#, Python, Kotlin, Scala, and Swift all support function types or SAM conversion. Closures can represent callable strategies and may capture state; that state still needs an ownership contract.
  • "Always use functions -- classes are ceremony." If the strategy needs named state, invariants, or multiple methods, classes are clearer. Choose according to visible invariants, ownership and testing needs, not a captured-variable threshold.
  • Function reference is not the same as inline lambda forever. Give a function a name when it communicates intent or supports useful reuse; call-site count is a heuristic. 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. Configuration lifetime? A closure or object may capture reusable configuration; per-request values can be explicit parameters in either representation.
  4. Testing? Both can be testable when their inputs, state and effects are visible. Evaluate the actual test seam.
  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. When does a one-method interface with one implementation earn its cost?
  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​

Contract and transfer​

A comparator has laws beyond (a,b) -> number: consistent self-comparison, sign reversal when arguments swap, and transitive ordering. A comparator that reads a changing clock may violate these laws during one sort. Fix the observation time before sorting if the policy requires a stable ranking. Decide how invalid dates and ties behave; a numeric-looking interface does not settle that policy.

The TypeScript sample assumes a supplied users array with valid dates. The shallow copy protects array order but still shares User objects. Mutating a user's name through the result mutates that same user seen by the caller. A closure capturing mutable configuration observes later changes unless it copies/freezes the relevant value.

Transfer: record the results of a captured discount after its configuration changes. Compare live configuration, captured immutable configuration, and an explicit per-call parameter. Document which behavior the client needs before choosing a function or object. P02 asks for this distinction; L01 checks the public contract.