Strategy Pattern
Define a family of algorithms, encapsulate each one as a function, and swap them at runtime.
The Problemβ
You're building an e-commerce checkout. Customers get different discounts: regular users pay full price, VIP members get 20% off, promo codes give 15% off.
The naive approach:
function calculatePrice(price: number, customerType: string): number {
if (customerType === "regular") return price;
if (customerType === "vip") return price * 0.8;
if (customerType === "promo") return price * 0.85;
// ... grows with every new discount type
}
Every new discount = modify this function. It becomes a maintenance nightmare.
The Solutionβ
Each discount becomes a standalone function. Pick the right one by key at runtime:
import { createStrategies } from "@pithos/core/eidos/strategy/strategy";
const pricing = createStrategies({
regular: (price: number) => price,
vip: (price: number) => price * 0.8,
promo: (price: number) => price * 0.85,
});
// TS infers: "regular" | "vip" | "promo" β typos are compile errors
pricing.execute("vip", 100); // 80
New discount? Add one line. No conditionals to update.
Live Demoβ
- strategies.ts
- Usage
/**
* Pricing strategies using Pithos Strategy pattern.
*
* This is the core of the Strategy pattern:
* each discount rule is a standalone function, selected by key at runtime.
* No interface, no concrete classes, no context β just functions in a record.
*/
import { createStrategies } from "@pithos/core/eidos/strategy/strategy";
import type { PricingInput, PricingResult, Product } from "./types";
/** Calculate subtotal from products */
function calculateSubtotal(products: Product[]): number {
return products.reduce((sum, p) => sum + p.basePrice * p.quantity, 0);
}
/** Apply a discount percentage to a subtotal */
function applyDiscount(subtotal: number, percent: number): PricingResult {
const discount = subtotal * (percent / 100);
return {
subtotal,
discount,
total: subtotal - discount,
discountPercent: percent,
};
}
export const pricingStrategies = createStrategies({
/** Regular pricing β no discount */
regular: ({ products }: PricingInput) =>
applyDiscount(calculateSubtotal(products), 0),
/** VIP pricing β 20% off everything */
vip: ({ products }: PricingInput) =>
applyDiscount(calculateSubtotal(products), 20),
/** Promo pricing β 15% off */
promo: ({ products }: PricingInput) =>
applyDiscount(calculateSubtotal(products), 15),
/** Bulk pricing β 25% off for orders over $300 */
bulk: ({ products }: PricingInput) => {
const subtotal = calculateSubtotal(products);
return applyDiscount(subtotal, subtotal >= 300 ? 25 : 0);
},
});
import { useState, useMemo, useCallback } from "react";
import { pricingStrategies } from "@/lib/strategies";
import { INITIAL_PRODUCTS } from "@/data/products";
import type { PricingStrategyKey, Product } from "@/lib/types";
export function usePricingCalculator() {
const [strategy, setStrategy] = useState<PricingStrategyKey>("regular");
const [products, setProducts] = useState<Product[]>(INITIAL_PRODUCTS);
const result = useMemo(
() => pricingStrategies.execute(strategy, { products }),
[strategy, products],
);
const updateQuantity = useCallback((index: number, quantity: number) => {
setProducts((prev) =>
prev.map((p, i) => (i === index ? { ...p, quantity: Math.max(0, quantity) } : p)),
);
}, []);
return { strategy, setStrategy, products, result, updateQuantity };
}
Real-World Analogyβ
A GPS app with multiple routing options: fastest, shortest, no tolls, scenic. Each is a separate algorithm, but the navigation UI doesn't care which one is active β it just calls "calculate route" on the selected strategy.
When to Use Itβ
- Multiple algorithms for the same task, selected at runtime
- You want to add new variants without modifying existing code
- Complex conditional logic that picks between similar operations
When NOT to Use Itβ
If you only have two fixed variants, a ternary is simpler. Strategy pays off when algorithms grow over time.
APIβ
- createStrategies β Build a strategy resolver from named functions
- safeStrategy β Wrap strategies to return
Resultinstead of throwing - withFallback β Chain a primary strategy with a backup
- withValidation β Validate input before execution