Strategy, when a function is already the pattern
In a language where functions are values, strategy is not a pattern you build. It is a parameter.
That is worth spelling out, because the pattern books devote a chapter to machinery JavaScript makes unnecessary — and recognising the shape still matters even when writing it takes one line.
The problem
function priceFor(plan, pieces) {
if (plan === "veg") {
return 8000 * pieces;
} else if (plan === "jain") {
return 9000 * pieces;
} else if (plan === "student") {
return Math.min(Math.round(7400 * pieces * 0.9), 180_000);
}
throw new Error(`unknown plan: ${plan}`);
}
Every new plan edits this function. Three unrelated pricing rules sit on top of each other. And it ends with a throw for a case that a lookup would make impossible.
The fix is an object of functions
const pricing = {
veg: (pieces) => 8000 * pieces,
jain: (pieces) => 9000 * pieces,
student: (pieces) => Math.min(Math.round(7400 * pieces * 0.9), 180_000),
};
function priceFor(plan, pieces) {
const rule = pricing[plan];
if (!rule) throw new Error(`unknown plan: ${plan}`);
return rule(pieces);
}
Adding a plan is one entry. Each rule is testable alone. The branching is gone.
That is the strategy pattern, complete. No interface, no classes, no factory — an object whose values are functions, which is how most JavaScript "strategies" are written.
A Map is better when the keys come from user input:
const pricing = new Map([["veg", …], ["jain", …]]);
pricing.get(plan);
because a plain object inherits from Object.prototype, so
pricing["toString"] returns a function rather than undefined — and a user
who types constructor as a plan name finds something. Object.create(null) or
a Map both avoid it, and with untrusted keys you want one of them.
You have been passing strategies all along
orders.sort((a, b) => a.placedAt.localeCompare(b.placedAt));
orders.filter((o) => o.pieces > 2);
orders.map((o) => o.customer);
JSON.parse(text, (key, value) => (key === "placedAt" ? new Date(value) : value));
sort does not know how to compare orders; it knows how to sort given something
that does. JSON.parse's second argument is a strategy for transforming values.
Every array method taking a callback is this pattern.
Before arrow functions this was already true, just noisier. It has never needed classes in JavaScript.
Named functions beat inline ones once they grow
// fine
orders.filter((o) => !o.delivered);
// not fine
orders.filter((o) => {
const slot = slotFor(o);
if (!slot) return false;
const [h] = slot.split(":");
return Number(h) >= 17 && !o.delivered && o.pieces > 0;
});
// better
const isPendingEveningOrder = (o) => { … };
orders.filter(isPendingEveningOrder);
A lambda spanning ten lines is a function that has not been given a name.
Naming it makes the filter call readable, makes the rule testable on its own,
and lets you reuse it.
Partial application
The JavaScript version of "a strategy that carries configuration":
const cappedAt = (cap) => (rate) => (pieces) =>
Math.min(Math.round(rate * pieces), cap);
const studentPricing = cappedAt(180_000)(7400);
studentPricing(40); // 180000
A Java strategy holding configuration needs a class with fields and a
constructor. Here it is a closure — the returned function remembers cap and
rate.
Do not overdo it. Three levels of arrow is clever and hard to read; two is often exactly right:
const cappedPricing = (rate, cap) => (pieces) =>
Math.min(Math.round(rate * pieces), cap);
const student = cappedPricing(7400, 180_000);
Default strategies
function renderOrders(orders, { sortBy = (a, b) => a.id.localeCompare(b.id) } = {}) {
return [...orders].sort(sortBy).map(row).join("");
}
A default parameter makes the strategy optional, which is the JavaScript equivalent of a template method's default step — and the whole reason this module does not have a template method lesson. With first-class functions and defaults, "a fixed skeleton with overridable steps" is a function taking optional callbacks:
function renderReport(orders, {
title = (n) => `${n} orders`,
row = (o) => `${o.customer}: ${o.pieces}`,
footer = () => "",
} = {}) {
return [title(orders.length), ...orders.map(row), footer()].join("\n");
}
That is a template method with no inheritance, no abstract class, and no subclass per variation.
The callback trap: losing this
class OrderList {
constructor() { this.orders = []; }
add(order) { this.orders.push(order); }
}
const list = new OrderList();
incoming.forEach(list.add); // TypeError: this.orders is undefined
Passing a method as a callback detaches it from its object. Three fixes:
incoming.forEach((o) => list.add(o)); // an arrow keeps the receiver
incoming.forEach(list.add.bind(list)); // bind it explicitly
class OrderList {
orders = [];
add = (order) => { this.orders.push(order); }; // a class field arrow
}
The third binds this permanently because a class field arrow captures the
instance at construction. It costs one function per instance — the factory
trade-off from the last lesson — and it is why React codebases are full of them.
This is the most common runtime error in JavaScript code that uses classes, and it is worth being able to recognise from the message alone.
When a class-based strategy is still right
Rare, but real: when a strategy needs several related methods rather than one.
const csvFormat = {
header: () => "customer,pieces",
row: (o) => `${o.customer},${o.pieces}`,
extension: "csv",
};
const jsonFormat = {
header: () => "[",
row: (o) => JSON.stringify(o),
extension: "json",
};
Still an object, still no class — but an object with several functions, not one. The moment a "strategy" needs two or more operations together, group them.
Check your work
Why strategy needs no machinery here: functions are values, so it is a parameter.
What it looks like in practice: an object whose values are functions.
Why a Map for user-supplied keys: a plain object inherits from
Object.prototype, so "toString" finds something.
Where you have already used it: sort, filter, map, JSON.parse's
reviver.
When to name an inline function: once it grows past a line or two, or needs a test.
What partial application replaces: a strategy class with configuration fields.
Why there is no template method lesson: optional callback parameters with defaults do the same job without inheritance.
The this trap: passing a method as a callback detaches it — fix with an
arrow, bind, or a class field arrow.
When to group strategies into an object: when the strategy needs more than one operation.
Practice
- Rewrite the
if/elsepricing as an object of functions. Add a fourth plan and count the lines you touched in each version. - Look up
pricing["toString"]on a plain object and on aMap. - Rebuild the lookup with
Object.create(null)and try again. - Find an inline callback in your capstone longer than three lines. Name it.
- Write
cappedPricing(7400, 180_000)and confirm the cap applies at 40 pieces. - Write
renderReportwith three default callbacks, then call it overriding onlyrow. - Reproduce the
thisbug by passing a class method toforEach. Read the exact error. - Fix it three ways and say which you would use in a class you own, and which in one you do not.
- Write a
csvFormatandjsonFormatobject pair and a function taking either. - Find a strategy in your capstone you did not know was one.
Next: observer, and the event system you already use.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship