iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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, ) { /* … */ }

Discussion

Loading…