Design PatternsBehavioral Patterns
Strategy
Define a family of algorithms, encapsulate each one, and make them interchangeable
Strategy Pattern
Intent
Strategy is a behavioral design pattern that lets you define a family of algorithms, put each of them into a separate class or function, and make their objects interchangeable. It enables selecting an algorithm at runtime.
Problem It Solves
When you have multiple algorithms for the same task:
The code becomes bloated and hard to maintain.
Solution
Extract algorithms into separate strategy objects:
The context delegates to the current strategy.
Implementation
/**
* Payment result
*/
interface PaymentResult {
success: boolean;
transactionId?: string;
error?: string;
fee: number;
}
/**
* Payment details
*/
interface PaymentDetails {
amount: number;
currency: string;
customerId: string;
metadata?: Record<string, string>;
}
/**
* Payment strategy interface
*/
interface PaymentStrategy {
name: string;
validate: (details: PaymentDetails) => boolean;
process: (details: PaymentDetails) => Promise<PaymentResult>;
calculateFee: (amount: number) => number;
}
/**
* Credit card payment strategy
*/
const createCreditCardStrategy = (apiKey: string): PaymentStrategy => ({
name: "Credit Card",
validate(details) {
return details.amount > 0 && details.amount <= 10000;
},
calculateFee(amount) {
return amount * 0.029 + 0.30; // 2.9% + $0.30
},
async process(details) {
console.log(`[Credit Card] Processing $${details.amount}...`);
await new Promise(r => setTimeout(r, 500));
return {
success: true,
transactionId: `cc_${Date.now()}`,
fee: this.calculateFee(details.amount),
};
},
});
/**
* PayPal payment strategy
*/
const createPayPalStrategy = (clientId: string): PaymentStrategy => ({
name: "PayPal",
validate(details) {
return details.amount > 0 && details.amount <= 25000;
},
calculateFee(amount) {
return amount * 0.034 + 0.49; // 3.4% + $0.49
},
async process(details) {
console.log(`[PayPal] Processing $${details.amount}...`);
await new Promise(r => setTimeout(r, 700));
return {
success: true,
transactionId: `pp_${Date.now()}`,
fee: this.calculateFee(details.amount),
};
},
});
/**
* Crypto payment strategy
*/
const createCryptoStrategy = (walletAddress: string): PaymentStrategy => ({
name: "Cryptocurrency",
validate(details) {
return details.amount > 0; // No upper limit
},
calculateFee(amount) {
return 1.00; // Flat $1 network fee
},
async process(details) {
console.log(`[Crypto] Processing $${details.amount}...`);
await new Promise(r => setTimeout(r, 2000)); // Blockchain is slower
return {
success: true,
transactionId: `btc_${Date.now()}`,
fee: this.calculateFee(details.amount),
};
},
});
/**
* Bank transfer strategy
*/
const createBankTransferStrategy = (): PaymentStrategy => ({
name: "Bank Transfer",
validate(details) {
return details.amount >= 100; // Minimum $100
},
calculateFee(amount) {
return Math.min(amount * 0.005, 5); // 0.5% capped at $5
},
async process(details) {
console.log(`[Bank Transfer] Processing $${details.amount}...`);
await new Promise(r => setTimeout(r, 300));
return {
success: true,
transactionId: `ach_${Date.now()}`,
fee: this.calculateFee(details.amount),
};
},
});
/**
* Payment processor (Context)
* @description Processes payments using configurable strategies
*/
interface PaymentProcessor {
setStrategy: (strategy: PaymentStrategy) => void;
getStrategy: () => PaymentStrategy | null;
process: (details: PaymentDetails) => Promise<PaymentResult>;
previewFee: (amount: number) => number;
}
const createPaymentProcessor = (): PaymentProcessor => {
let currentStrategy: PaymentStrategy | null = null;
return {
setStrategy(strategy) {
currentStrategy = strategy;
console.log(`Payment method set to: ${strategy.name}`);
},
getStrategy() {
return currentStrategy;
},
async process(details) {
if (!currentStrategy) {
return { success: false, error: "No payment method selected", fee: 0 };
}
if (!currentStrategy.validate(details)) {
return {
success: false,
error: `Invalid payment for ${currentStrategy.name}`,
fee: 0
};
}
return currentStrategy.process(details);
},
previewFee(amount) {
if (!currentStrategy) return 0;
return currentStrategy.calculateFee(amount);
},
};
};
// Usage
const processor = createPaymentProcessor();
// Available strategies
const creditCard = createCreditCardStrategy("sk_test_xxx");
const paypal = createPayPalStrategy("client_xxx");
const crypto = createCryptoStrategy("0x123...");
const bankTransfer = createBankTransferStrategy();
const paymentDetails: PaymentDetails = {
amount: 99.99,
currency: "USD",
customerId: "cust_123",
};
console.log("\n--- Payment Processing Demo ---\n");
// Compare fees
console.log("Fee comparison for $99.99:");
console.log(` Credit Card: $${creditCard.calculateFee(99.99).toFixed(2)}`);
console.log(` PayPal: $${paypal.calculateFee(99.99).toFixed(2)}`);
console.log(` Crypto: $${crypto.calculateFee(99.99).toFixed(2)}`);
console.log(` Bank Transfer: $${bankTransfer.calculateFee(99.99).toFixed(2)}`);
// Process with credit card
console.log("\n--- Processing with Credit Card ---\n");
processor.setStrategy(creditCard);
const result1 = await processor.process(paymentDetails);
console.log("Result:", result1);
// ^?
// Switch to PayPal
console.log("\n--- Processing with PayPal ---\n");
processor.setStrategy(paypal);
const result2 = await processor.process({ ...paymentDetails, amount: 150 });
console.log("Result:", result2);/**
* Comparison function type
*/
type Comparator<T> = (a: T, b: T) => number;
/**
* Sorting strategy interface
*/
interface SortingStrategy<T> {
name: string;
sort: (array: T[], compare: Comparator<T>) => T[];
complexity: { best: string; average: string; worst: string };
stable: boolean;
}
/**
* Bubble sort strategy
*/
const bubbleSort = <T>(): SortingStrategy<T> => ({
name: "Bubble Sort",
complexity: { best: "O(n)", average: "O(n²)", worst: "O(n²)" },
stable: true,
sort(array, compare) {
const result = [...array];
const n = result.length;
for (let i = 0; i < n - 1; i++) {
let swapped = false;
for (let j = 0; j < n - i - 1; j++) {
if (compare(result[j], result[j + 1]) > 0) {
[result[j], result[j + 1]] = [result[j + 1], result[j]];
swapped = true;
}
}
if (!swapped) break;
}
return result;
},
});
/**
* Quick sort strategy
*/
const quickSort = <T>(): SortingStrategy<T> => ({
name: "Quick Sort",
complexity: { best: "O(n log n)", average: "O(n log n)", worst: "O(n²)" },
stable: false,
sort(array, compare) {
if (array.length <= 1) return [...array];
const pivot = array[Math.floor(array.length / 2)];
const left = array.filter(x => compare(x, pivot) < 0);
const middle = array.filter(x => compare(x, pivot) === 0);
const right = array.filter(x => compare(x, pivot) > 0);
return [
...this.sort(left, compare),
...middle,
...this.sort(right, compare),
];
},
});
/**
* Merge sort strategy
*/
const mergeSort = <T>(): SortingStrategy<T> => ({
name: "Merge Sort",
complexity: { best: "O(n log n)", average: "O(n log n)", worst: "O(n log n)" },
stable: true,
sort(array, compare) {
if (array.length <= 1) return [...array];
const mid = Math.floor(array.length / 2);
const left = this.sort(array.slice(0, mid), compare);
const right = this.sort(array.slice(mid), compare);
// Merge
const result: T[] = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (compare(left[i], right[j]) <= 0) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return [...result, ...left.slice(i), ...right.slice(j)];
},
});
/**
* Insertion sort strategy
*/
const insertionSort = <T>(): SortingStrategy<T> => ({
name: "Insertion Sort",
complexity: { best: "O(n)", average: "O(n²)", worst: "O(n²)" },
stable: true,
sort(array, compare) {
const result = [...array];
for (let i = 1; i < result.length; i++) {
const key = result[i];
let j = i - 1;
while (j >= 0 && compare(result[j], key) > 0) {
result[j + 1] = result[j];
j--;
}
result[j + 1] = key;
}
return result;
},
});
/**
* Sorter context
*/
interface Sorter<T> {
setStrategy: (strategy: SortingStrategy<T>) => void;
sort: (array: T[], compare?: Comparator<T>) => T[];
getInfo: () => { name: string; complexity: { best: string; average: string; worst: string }; stable: boolean } | null;
}
const createSorter = <T>(): Sorter<T> => {
let strategy: SortingStrategy<T> | null = null;
const defaultCompare: Comparator<T> = (a, b) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
};
return {
setStrategy(s) {
strategy = s;
},
sort(array, compare = defaultCompare) {
if (!strategy) {
throw new Error("No sorting strategy set");
}
return strategy.sort(array, compare);
},
getInfo() {
if (!strategy) return null;
return {
name: strategy.name,
complexity: strategy.complexity,
stable: strategy.stable,
};
},
};
};
// Usage
const sorter = createSorter<number>();
const testArray = [64, 34, 25, 12, 22, 11, 90];
console.log("\n--- Sorting Strategies Demo ---\n");
console.log("Original:", testArray);
// Test each strategy
const strategies = [
bubbleSort<number>(),
insertionSort<number>(),
quickSort<number>(),
mergeSort<number>(),
];
for (const strategy of strategies) {
sorter.setStrategy(strategy);
const sorted = sorter.sort([...testArray]);
const info = sorter.getInfo();
console.log(`\n${info?.name}:`);
console.log(` Result: [${sorted.join(", ")}]`);
console.log(` Complexity: ${info?.complexity.average}`);
console.log(` Stable: ${info?.stable}`);
}
// Custom comparator (descending)
console.log("\n--- Custom Comparator (Descending) ---\n");
sorter.setStrategy(mergeSort());
const descending = sorter.sort(testArray, (a, b) => b - a);
console.log("Descending:", descending);/**
* Compression result
*/
interface CompressionResult {
data: Uint8Array;
originalSize: number;
compressedSize: number;
ratio: number;
algorithm: string;
}
/**
* Compression strategy interface
*/
interface CompressionStrategy {
name: string;
compress: (data: string) => CompressionResult;
decompress: (data: Uint8Array) => string;
getLevel: () => number;
setLevel: (level: number) => void;
}
/**
* Simple RLE (Run-Length Encoding) compression
*/
const createRLEStrategy = (): CompressionStrategy => {
let level = 1; // Not used in RLE, but for interface consistency
return {
name: "RLE",
getLevel: () => level,
setLevel: (l) => { level = l; },
compress(data) {
const encoder = new TextEncoder();
const original = encoder.encode(data);
// Simple RLE: count consecutive characters
let compressed = "";
let count = 1;
for (let i = 0; i < data.length; i++) {
if (data[i] === data[i + 1]) {
count++;
} else {
compressed += count > 1 ? `${count}${data[i]}` : data[i];
count = 1;
}
}
const result = encoder.encode(compressed);
return {
data: result,
originalSize: original.length,
compressedSize: result.length,
ratio: result.length / original.length,
algorithm: this.name,
};
},
decompress(data) {
const decoder = new TextDecoder();
const str = decoder.decode(data);
let result = "";
let count = "";
for (const char of str) {
if (/\d/.test(char)) {
count += char;
} else {
const times = count ? parseInt(count) : 1;
result += char.repeat(times);
count = "";
}
}
return result;
},
};
};
/**
* LZ77-like compression (simplified)
*/
const createLZ77Strategy = (): CompressionStrategy => {
let windowSize = 255;
return {
name: "LZ77",
getLevel: () => Math.floor(windowSize / 85), // 1-3
setLevel: (level) => { windowSize = level * 85; },
compress(data) {
const encoder = new TextEncoder();
const original = encoder.encode(data);
// Simplified: Just demonstrate the concept
const tokens: number[] = [];
let i = 0;
while (i < data.length) {
let bestMatch = { offset: 0, length: 0 };
const windowStart = Math.max(0, i - windowSize);
// Look for matches in window
for (let j = windowStart; j < i; j++) {
let length = 0;
while (
i + length < data.length &&
data[j + length] === data[i + length] &&
length < 15 // Max match length
) {
length++;
}
if (length > bestMatch.length) {
bestMatch = { offset: i - j, length };
}
}
if (bestMatch.length >= 3) {
// Store as: [flag=1, offset, length]
tokens.push(1, bestMatch.offset, bestMatch.length);
i += bestMatch.length;
} else {
// Store as: [flag=0, char]
tokens.push(0, data.charCodeAt(i));
i++;
}
}
const result = new Uint8Array(tokens);
return {
data: result,
originalSize: original.length,
compressedSize: result.length,
ratio: result.length / original.length,
algorithm: this.name,
};
},
decompress(data) {
let result = "";
let i = 0;
while (i < data.length) {
if (data[i] === 0) {
result += String.fromCharCode(data[i + 1]);
i += 2;
} else {
const offset = data[i + 1];
const length = data[i + 2];
const start = result.length - offset;
for (let j = 0; j < length; j++) {
result += result[start + j];
}
i += 3;
}
}
return result;
},
};
};
/**
* No compression (passthrough)
*/
const createNoCompressionStrategy = (): CompressionStrategy => ({
name: "None",
getLevel: () => 0,
setLevel: () => {},
compress(data) {
const encoder = new TextEncoder();
const bytes = encoder.encode(data);
return {
data: bytes,
originalSize: bytes.length,
compressedSize: bytes.length,
ratio: 1,
algorithm: this.name,
};
},
decompress(data) {
return new TextDecoder().decode(data);
},
});
/**
* Compressor context
*/
interface Compressor {
setStrategy: (strategy: CompressionStrategy) => void;
compress: (data: string) => CompressionResult;
decompress: (data: Uint8Array) => string;
setLevel: (level: number) => void;
}
const createCompressor = (): Compressor => {
let strategy: CompressionStrategy = createNoCompressionStrategy();
return {
setStrategy(s) {
strategy = s;
console.log(`Compression set to: ${s.name}`);
},
compress(data) {
return strategy.compress(data);
},
decompress(data) {
return strategy.decompress(data);
},
setLevel(level) {
strategy.setLevel(level);
},
};
};
// Usage
const compressor = createCompressor();
const testData = "AAABBBCCCCCDDDDDDEEEEEEEFFFFFFFF";
console.log("\n--- Compression Strategies Demo ---\n");
console.log(`Original: "${testData}" (${testData.length} bytes)\n`);
const strategies = [
createNoCompressionStrategy(),
createRLEStrategy(),
createLZ77Strategy(),
];
for (const strategy of strategies) {
compressor.setStrategy(strategy);
const result = compressor.compress(testData);
const decompressed = compressor.decompress(result.data);
console.log(`${result.algorithm}:`);
console.log(` Compressed: ${result.compressedSize} bytes`);
console.log(` Ratio: ${(result.ratio * 100).toFixed(1)}%`);
console.log(` Decompressed matches: ${decompressed === testData}`);
console.log();
}/**
* Package dimensions and weight
*/
interface Package {
weight: number; // kg
length: number; // cm
width: number; // cm
height: number; // cm
}
/**
* Shipping address
*/
interface ShippingAddress {
country: string;
state: string;
city: string;
postalCode: string;
isResidential: boolean;
}
/**
* Shipping quote
*/
interface ShippingQuote {
carrier: string;
service: string;
price: number;
estimatedDays: number;
tracking: boolean;
insurance: boolean;
}
/**
* Shipping strategy interface
*/
interface ShippingStrategy {
carrier: string;
calculateRate: (pkg: Package, from: ShippingAddress, to: ShippingAddress) => ShippingQuote[];
isAvailable: (from: ShippingAddress, to: ShippingAddress) => boolean;
}
/**
* Calculate volumetric weight
*/
const volumetricWeight = (pkg: Package, divisor = 5000): number => {
return (pkg.length * pkg.width * pkg.height) / divisor;
};
/**
* Get billable weight
*/
const billableWeight = (pkg: Package): number => {
return Math.max(pkg.weight, volumetricWeight(pkg));
};
/**
* UPS shipping strategy
*/
const createUPSStrategy = (): ShippingStrategy => ({
carrier: "UPS",
isAvailable(from, to) {
// UPS available in most countries
return true;
},
calculateRate(pkg, from, to) {
const weight = billableWeight(pkg);
const isInternational = from.country !== to.country;
const baseRate = isInternational ? 25 : 8;
return [
{
carrier: "UPS",
service: "Ground",
price: baseRate + weight * 1.5,
estimatedDays: isInternational ? 7 : 5,
tracking: true,
insurance: false,
},
{
carrier: "UPS",
service: "Express",
price: baseRate * 2 + weight * 3,
estimatedDays: isInternational ? 3 : 2,
tracking: true,
insurance: true,
},
{
carrier: "UPS",
service: "Next Day Air",
price: baseRate * 4 + weight * 5,
estimatedDays: 1,
tracking: true,
insurance: true,
},
];
},
});
/**
* FedEx shipping strategy
*/
const createFedExStrategy = (): ShippingStrategy => ({
carrier: "FedEx",
isAvailable(from, to) {
return true;
},
calculateRate(pkg, from, to) {
const weight = billableWeight(pkg);
const isInternational = from.country !== to.country;
const baseRate = isInternational ? 28 : 9;
const quotes: ShippingQuote[] = [
{
carrier: "FedEx",
service: "Ground",
price: baseRate + weight * 1.4,
estimatedDays: isInternational ? 8 : 5,
tracking: true,
insurance: false,
},
{
carrier: "FedEx",
service: "Express Saver",
price: baseRate * 1.8 + weight * 2.8,
estimatedDays: isInternational ? 4 : 3,
tracking: true,
insurance: false,
},
{
carrier: "FedEx",
service: "Priority Overnight",
price: baseRate * 3.5 + weight * 4.5,
estimatedDays: 1,
tracking: true,
insurance: true,
},
];
// FedEx offers SmartPost for residential
if (to.isResidential) {
quotes.unshift({
carrier: "FedEx",
service: "SmartPost",
price: baseRate * 0.7 + weight * 1.0,
estimatedDays: isInternational ? 10 : 7,
tracking: true,
insurance: false,
});
}
return quotes;
},
});
/**
* USPS shipping strategy
*/
const createUSPSStrategy = (): ShippingStrategy => ({
carrier: "USPS",
isAvailable(from, to) {
// USPS primarily US
return from.country === "US";
},
calculateRate(pkg, from, to) {
const weight = billableWeight(pkg);
const isInternational = from.country !== to.country;
if (isInternational) {
return [
{
carrier: "USPS",
service: "Priority Mail International",
price: 30 + weight * 2,
estimatedDays: 10,
tracking: true,
insurance: false,
},
];
}
return [
{
carrier: "USPS",
service: "First Class",
price: 4 + weight * 0.8,
estimatedDays: 5,
tracking: true,
insurance: false,
},
{
carrier: "USPS",
service: "Priority Mail",
price: 8 + weight * 1.2,
estimatedDays: 3,
tracking: true,
insurance: false,
},
{
carrier: "USPS",
service: "Priority Mail Express",
price: 25 + weight * 2,
estimatedDays: 1,
tracking: true,
insurance: true,
},
];
},
});
/**
* Shipping calculator (Context)
*/
interface ShippingCalculator {
addCarrier: (strategy: ShippingStrategy) => void;
removeCarrier: (carrierName: string) => void;
getQuotes: (pkg: Package, from: ShippingAddress, to: ShippingAddress) => ShippingQuote[];
getBestQuote: (pkg: Package, from: ShippingAddress, to: ShippingAddress) => ShippingQuote | null;
getFastestQuote: (pkg: Package, from: ShippingAddress, to: ShippingAddress) => ShippingQuote | null;
}
const createShippingCalculator = (): ShippingCalculator => {
const carriers: ShippingStrategy[] = [];
return {
addCarrier(strategy) {
carriers.push(strategy);
console.log(`Added carrier: ${strategy.carrier}`);
},
removeCarrier(carrierName) {
const index = carriers.findIndex(c => c.carrier === carrierName);
if (index >= 0) {
carriers.splice(index, 1);
}
},
getQuotes(pkg, from, to) {
const allQuotes: ShippingQuote[] = [];
for (const carrier of carriers) {
if (carrier.isAvailable(from, to)) {
allQuotes.push(...carrier.calculateRate(pkg, from, to));
}
}
// Sort by price
return allQuotes.sort((a, b) => a.price - b.price);
},
getBestQuote(pkg, from, to) {
const quotes = this.getQuotes(pkg, from, to);
return quotes[0] ?? null;
},
getFastestQuote(pkg, from, to) {
const quotes = this.getQuotes(pkg, from, to);
return quotes.sort((a, b) => a.estimatedDays - b.estimatedDays)[0] ?? null;
},
};
};
// Usage
const calculator = createShippingCalculator();
// Add carriers
calculator.addCarrier(createUPSStrategy());
calculator.addCarrier(createFedExStrategy());
calculator.addCarrier(createUSPSStrategy());
const pkg: Package = {
weight: 2.5,
length: 30,
width: 20,
height: 15,
};
const fromAddress: ShippingAddress = {
country: "US",
state: "CA",
city: "Los Angeles",
postalCode: "90001",
isResidential: false,
};
const toAddress: ShippingAddress = {
country: "US",
state: "NY",
city: "New York",
postalCode: "10001",
isResidential: true,
};
console.log("\n--- Shipping Calculator Demo ---\n");
console.log("Package:", pkg);
console.log("From:", `${fromAddress.city}, ${fromAddress.state}`);
console.log("To:", `${toAddress.city}, ${toAddress.state}`);
console.log("\n--- All Quotes (sorted by price) ---\n");
const quotes = calculator.getQuotes(pkg, fromAddress, toAddress);
for (const quote of quotes) {
console.log(
`${quote.carrier} ${quote.service}: $${quote.price.toFixed(2)} ` +
`(${quote.estimatedDays} days)`
);
}
console.log("\n--- Best (Cheapest) Quote ---");
const best = calculator.getBestQuote(pkg, fromAddress, toAddress);
if (best) {
console.log(`${best.carrier} ${best.service}: $${best.price.toFixed(2)}`);
}
console.log("\n--- Fastest Quote ---");
const fastest = calculator.getFastestQuote(pkg, fromAddress, toAddress);
if (fastest) {
console.log(
`${fastest.carrier} ${fastest.service}: ` +
`${fastest.estimatedDays} day(s) - $${fastest.price.toFixed(2)}`
);
}When to Use
Strategy in Functional Style
In JavaScript/TypeScript, strategies can be simple functions:
// Strategies as functions
type SortStrategy<T> = (arr: T[]) => T[];
const bubbleSort: SortStrategy<number> = (arr) => { /* ... */ return arr; };
const quickSort: SortStrategy<number> = (arr) => { /* ... */ return arr; };
// Usage - just pass the function
const sorted = quickSort([3, 1, 2]);Summary
Key Takeaway: Strategy defines a family of interchangeable algorithms, letting clients select and switch between them at runtime without changing the code that uses them.
Pros
- ✅ Swappable algorithms at runtime
- ✅ Isolates algorithm implementation
- ✅ Open/Closed: Add strategies without modifying context
- ✅ Eliminates conditional statements
Cons
- ❌ Clients must know about strategies
- ❌ Overhead for simple algorithms
- ❌ Increased number of objects/functions