// ───────────────────────────────────────────────────────────────────────────── // STRATEGY [Behavioral] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Define a family of algorithms, encapsulate each one, and make them interchangeable; let the // algorithm vary independently from the clients that use it. // PROBLEM : `switch (shippingMethod)` / `if (provider === 'stripe')` blocks duplicated across the codebase; // adding a method means editing every switch. // SAY : Participants: Context (holds a Strategy, delegates to it), Strategy (common interface, e.g. // calculate(order)), ConcreteStrategy (FlatRate, WeightBased, ...). Pitch: "plug in the algorithm // — same call, different implementation, chosen at runtime." // SEEN IN : `Array.prototype.sort(comparator)`; Passport.js strategies (`passport.use(new GoogleStrategy())`); // payment/auth provider maps (`providers[name].charge()`); Multer storage engines // (`diskStorage` / `memoryStorage`); Express view engines (`app.engine('hbs', ...)`); retry/backoff // policies; bcrypt vs argon2 behind one `hash()`. // VS : State: strategies are picked by the CLIENT and don't know each other; states switch THEMSELVES. // Template Method: varies STEPS via inheritance; Strategy varies the WHOLE algorithm via composition. // Command: a request to do something (often with undo); Strategy: how to compute something. // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) // Strategy interface: calculate(order) -> cents class FlatRateShipping { // ConcreteStrategy constructor(cents) { this.cents = cents; } calculate() { return this.cents; } } class WeightBasedShipping { // ConcreteStrategy constructor(centsPerKg, minimum) { this.centsPerKg = centsPerKg; this.minimum = minimum; } calculate(order) { return Math.max(this.minimum, Math.ceil(order.weightKg * this.centsPerKg)); } } class FreeOverThreshold { // ConcreteStrategy wrapping another — strategies compose constructor(thresholdCents, fallback) { this.thresholdCents = thresholdCents; this.fallback = fallback; } calculate(order) { return order.subtotalCents >= this.thresholdCents ? 0 : this.fallback.calculate(order); } } class Checkout { // Context constructor(shipping) { this.shipping = shipping; } setShipping(s) { this.shipping = s; return this; } // swappable at runtime total(order) { return order.subtotalCents + this.shipping.calculate(order); } } const order = { subtotalCents: 8000, weightKg: 2.5 }; const checkout = new Checkout(new FlatRateShipping(599)); assert.equal(checkout.total(order), 8599); assert.equal(checkout.setShipping(new WeightBasedShipping(300, 500)).total(order), 8750); // 2.5kg × 300 assert.equal(checkout.total({ ...order, weightKg: 0.5 }), 8500); // minimum applies const promo = new FreeOverThreshold(5000, new FlatRateShipping(599)); assert.equal(checkout.setShipping(promo).total(order), 8000); assert.equal(checkout.total({ ...order, subtotalCents: 2000 }), 2599); // ── Idiomatic JS shape: a plain object map of functions, selected by key const pricingStrategies = { standard: (cart) => cart.subtotal, member: (cart) => Math.round(cart.subtotal * 0.9), bulk: (cart) => (cart.qty >= 10 ? Math.round(cart.subtotal * 0.8) : cart.subtotal), coupon: (cart, { off }) => Math.max(0, cart.subtotal - off), }; const price = (cart, strategy, opts) => { const fn = pricingStrategies[strategy]; if (!fn) throw new Error(`unknown pricing strategy: ${strategy}`); return fn(cart, opts); }; const cart = { subtotal: 10000, qty: 12 }; assert.equal(price(cart, 'standard'), 10000); assert.equal(price(cart, 'member'), 9000); assert.equal(price(cart, 'bulk'), 8000); assert.equal(price({ subtotal: 10000, qty: 2 }, 'bulk'), 10000); assert.equal(price(cart, 'coupon', { off: 2500 }), 7500); assert.throws(() => price(cart, 'vip'), /unknown pricing strategy/); assert.deepEqual(Object.keys(pricingStrategies), ['standard', 'member', 'bulk', 'coupon']); // add one = add a key // Auth providers — the everyday strategy object in Node backends (same verify() shape each) const authStrategies = { password: { verify: ({ user, password }) => user.hash === `h:${password}` }, apiKey: { verify: ({ key }, keys) => keys.has(key) }, oauth: { verify: ({ token }) => token.startsWith('ya29.') }, }; const authenticate = (kind, creds, deps) => authStrategies[kind]?.verify(creds, deps) ?? false; assert.equal(authenticate('password', { user: { hash: 'h:pw' }, password: 'pw' }), true); assert.equal(authenticate('apiKey', { key: 'k1' }, new Set(['k1'])), true); assert.equal(authenticate('oauth', { token: 'nope' }), false); assert.equal(authenticate('saml', {}), false); // unknown strategy → denied, not crashed // The built-in one everybody has used: sort with a comparator strategy const users = [{ name: 'bo', age: 40 }, { name: 'Al', age: 30 }]; const byAge = (a, b) => a.age - b.age; const byName = (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); assert.deepEqual([...users].sort(byAge).map((u) => u.name), ['Al', 'bo']); assert.deepEqual([...users].sort(byName).map((u) => u.name), ['Al', 'bo']); assert.deepEqual([...users].sort((a, b) => byAge(b, a)).map((u) => u.name), ['bo', 'Al']); // strategies compose console.log('ok: design-patterns/behavioral/strategy.js');