Strategy
The Strategy pattern lets you swap algorithms at runtime by injecting a strategy object. Avoids long switch blocks and makes new behaviour testable in isolation.
Pricing, sorting, payments examples
EXAMPLE
// 1) Classic OO — interface + concrete strategies
interface DiscountStrategy {
apply(amount: number, items: number): number;
}
class NoDiscount implements DiscountStrategy {
apply(amount: number) { return amount; }
}
class PercentDiscount implements DiscountStrategy {
constructor(private percent: number) {}
apply(amount: number) { return amount * (1 - this.percent / 100); }
}
class BulkDiscount implements DiscountStrategy {
constructor(private threshold: number, private percent: number) {}
apply(amount: number, items: number) {
return items >= this.threshold ? amount * (1 - this.percent / 100) : amount;
}
}
class Cart {
constructor(private items: { qty: number; price: number }[], private discount: DiscountStrategy = new NoDiscount()) {}
setDiscount(d: DiscountStrategy) { this.discount = d; }
total() {
const totalQty = this.items.reduce((s, i) => s + i.qty, 0);
const totalPrice = this.items.reduce((s, i) => s + i.qty * i.price, 0);
return this.discount.apply(totalPrice, totalQty);
}
}
const cart = new Cart(items);
cart.setDiscount(new BulkDiscount(10, 15)); // 15% off when 10+ items
console.log(cart.total());
// 2) Functional flavour — strategy is just a function
type Discount = (amount: number, items: number) => number;
const none: Discount = (a) => a;
const percent = (p: number): Discount => (a) => a * (1 - p / 100);
const bulk = (t: number, p: number): Discount => (a, n) => n >= t ? a * (1 - p / 100) : a;
const checkout = (items, d: Discount = none) => {
const qty = items.reduce((s, i) => s + i.qty, 0);
const total = items.reduce((s, i) => s + i.qty * i.price, 0);
return d(total, qty);
};
// 3) Strategy from configuration
const STRATEGIES: Record<string, DiscountStrategy> = {
none: new NoDiscount(),
welcome: new PercentDiscount(10),
vip: new PercentDiscount(20),
bulk: new BulkDiscount(10, 15),
};
function priceCart(items, strategyName: string) {
const strategy = STRATEGIES[strategyName] ?? new NoDiscount();
return new Cart(items, strategy).total();
}
// 4) Real example — payment providers
interface PaymentProvider {
name: string;
charge(amount: number, currency: string): Promise<{ ok: boolean; reference: string }>;
refund(reference: string): Promise<void>;
}
class StripeProvider implements PaymentProvider { /* … */ }
class PayPalProvider implements PaymentProvider { /* … */ }
class MockProvider implements PaymentProvider { /* … */ }
class Checkout {
constructor(private payments: PaymentProvider) {}
async pay(order: Order) {
const r = await this.payments.charge(order.total, order.currency);
if (!r.ok) throw new Error('charge failed');
return r.reference;
}
}
// At wire-up time, inject the right one:
new Checkout(env.PROD ? new StripeProvider(stripeKey) : new MockProvider()).pay(order);
// 5) Strategy as a Map for table-driven dispatch (replaces switch)
type Method = (req, res) => unknown;
const handlers: Record<string, Method> = {
GET: handleGet,
POST: handlePost,
PUT: handlePut,
DELETE: handleDelete,
};
function route(req, res) {
const fn = handlers[req.method];
return fn ? fn(req, res) : res.status(405).end();
}
// 6) Sort with a comparator (classic strategy)
// Sort by name, then by age, then by salary — strategies passed in
const byName = (a, b) => a.name.localeCompare(b.name);
const byAge = (a, b) => a.age - b.age;
const bySalary = (a, b) => b.salary - a.salary; // descending
function multiSort<T>(arr: T[], ...cmps: Array<(a: T, b: T) => number>): T[] {
return [...arr].sort((a, b) => cmps.reduce((acc, fn) => acc || fn(a, b), 0));
}
multiSort(users, byName, byAge);
multiSort(users, bySalary, byName);
// 7) Logging / serialisation / caching — anywhere policy varies
interface Cache<T> {
get(key: string): Promise<T | null>;
set(key: string, value: T, ttl: number): Promise<void>;
}
class InMemoryCache<T> implements Cache<T> { /* … */ }
class RedisCache<T> implements Cache<T> { /* … */ }
class NullCache<T> implements Cache<T> { async get(){return null;} async set(){} }
function makeService<T>(cache: Cache<T>) { /* uses cache without knowing the impl */ }
// 8) When NOT to use Strategy
// • Only ONE strategy exists right now and adding more is hypothetical
// • The variants are tiny enough that a switch is cleaner
// • You don't need to swap at runtime (compile-time choice = generic / template)
// 9) Why it pays off
// • New variants don't touch existing code (Open-Closed)
// • Each strategy is independently testable
// • Production / test injection trivial
// • Configuration-driven behaviour (env-based, feature-flag-based)
// 10) Real-world signs you NEED Strategy
// • A switch / if-elif chain on a 'kind' field that's repeated in multiple places
// • Tests that need to mock a vendor / provider / external service
// • Feature flags that change algorithms (A/B test of recommendation logic)
// • Pluggable extension points (themes, payment methods, cache backends)
Why it matters
Strategy is functional programming in OO clothing. Inject a behaviour (object or function); the host calls a single contract. Replace switch chains with a dictionary of strategies and tests become trivial.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
function totalWithDiscount(items, discount) { return discount(items.reduce((s, i) => s + i.price, 0)); }
const noDiscount = total => total;
const tenPercent = total => total * 0.9;
const flatTen = total => total - 10;
Try it Yourself »
Exercise
Strategy passes the algorithm as a…
function total(items,
) { /* … */ }
Eight letters.
Discussion
Loading…