A checkout has to add shipping. The rule depends on the carrier: some charge a flat rate, some charge by weight, some waive it above a threshold.
What people write first
class Checkout { total(order: Order, carrier: string): number { let shipping: number;
if (carrier === 'flat') shipping = 5; else if (carrier === 'weight') shipping = Math.ceil(order.weightKg) * 2.5; else if (carrier === 'free') shipping = order.subtotal >= 50 ? 0 : 7; else throw new Error(`unknown carrier: ${carrier}`);
return order.subtotal + shipping; }}This is fine. Genuinely — for three rules that never change, stop here.
It becomes a problem when the rules keep coming. Each new carrier means editing
a class that is already correct and already tested, and the branch tends to
reappear: once in total, again in estimate, again in the checkout summary.
Now adding a carrier means finding all three.
The pattern
Make the varying part an object, and hand it in.
interface ShippingRule { cost(order: Order): number;}
class FlatRate implements ShippingRule { cost() { return 5; }}
class ByWeight implements ShippingRule { cost(order: Order) { return Math.ceil(order.weightKg) * 2.5; }}
class Checkout { constructor(private readonly shipping: ShippingRule) {}
total(order: Order) { return order.subtotal + this.shipping.cost(order); }}Switch the rule and watch only the rule change:
// the only thing that varies cost(order) { return 5; }
- subtotal
- £84.00
- shipping
- £5.00
- total
- £89.00
The Checkout class is identical in all three. It never learns which rule it is
using, which is the entire point — a fourth carrier is a new object, not a new branch inside
code that already works.
In this language, it is a function
The interface has one method. In any language with first-class functions, that is a function type, and the whole class hierarchy collapses:
type ShippingRule = (order: Order) => number;
const flatRate: ShippingRule = () => 5;const byWeight: ShippingRule = (o) => Math.ceil(o.weightKg) * 2.5;const freeOver = (limit: number): ShippingRule => (o) => (o.subtotal >= limit ? 0 : 7);
class Checkout { constructor(private readonly shipping: ShippingRule) {} total(order: Order) { return order.subtotal + this.shipping(order); }}
new Checkout(freeOver(50)).total(order);Same pattern, same benefit, none of the ceremony. freeOver is a parameterised
strategy — the thing that would otherwise need a constructor and a private
field.
Reach for the class form when the strategy needs more than one method, when it holds meaningful state, or when your codebase’s conventions expect it. A single function is otherwise better.
Where you already use it
array.sort(comparator) is Strategy. So is every middleware function, every
React render prop, every onClick handler, and every retry policy passed to an
HTTP client. You have been writing it for years.
Neighbours
State has the same structure — an object holding varying behaviour — and a different intent. Strategies are chosen by the caller and do not know about each other; states swap themselves in response to events.
Template Method solves the same “one algorithm, varying steps” problem with inheritance rather than composition: the steps are overridden methods instead of an injected object. Strategy composes, so it can change at runtime; Template Method is fixed at compile time.