RizTech Academy logo
RizTech Academy
Design Patterns in JavaScriptLesson 4 of 630 min

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

  1. Rewrite the if/else pricing as an object of functions. Add a fourth plan and count the lines you touched in each version.
  2. Look up pricing["toString"] on a plain object and on a Map.
  3. Rebuild the lookup with Object.create(null) and try again.
  4. Find an inline callback in your capstone longer than three lines. Name it.
  5. Write cappedPricing(7400, 180_000) and confirm the cap applies at 40 pieces.
  6. Write renderReport with three default callbacks, then call it overriding only row.
  7. Reproduce the this bug by passing a class method to forEach. Read the exact error.
  8. Fix it three ways and say which you would use in a class you own, and which in one you do not.
  9. Write a csvFormat and jsonFormat object pair and a function taking either.
  10. 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