Episode 9 — System Design / 9.5 — Behavioral Design Patterns
9.5.b Strategy Pattern
Overview
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
In plain terms: instead of hardcoding an algorithm into a class, you extract it into its own object and let the client swap between algorithms at runtime.
+-------------------------------------------------------------+
| STRATEGY PATTERN |
| |
| Context Strategies |
| +--------------------+ +---------+---------+ |
| | | | | | |
| | - strategy -------+--------->| Algo A | Algo B | |
| | | swap | | | |
| | + setStrategy() | ------>+---------+---------+ |
| | + execute() | | | | |
| | (delegates to | | Algo C | Algo D | |
| | strategy) | | | | |
| +--------------------+ +---------+---------+ |
| |
| "Define a family of algorithms, encapsulate each one, |
| and make them interchangeable." |
+-------------------------------------------------------------+
The Problem: Conditional Chaos
// BAD: Algorithm selection via if/else chains
class PaymentProcessor {
processPayment(amount, method, details) {
if (method === 'credit_card') {
// 30 lines of credit card processing logic
console.log(`Processing CC: ${details.cardNumber}`);
const fee = amount * 0.029 + 0.30;
// validate card, check expiry, call gateway...
return { success: true, fee, total: amount + fee };
} else if (method === 'paypal') {
// 25 lines of PayPal logic
console.log(`Processing PayPal: ${details.email}`);
const fee = amount * 0.035;
// redirect to PayPal, wait for callback...
return { success: true, fee, total: amount + fee };
} else if (method === 'crypto') {
// 35 lines of crypto logic
console.log(`Processing Crypto: ${details.walletAddress}`);
const fee = amount * 0.01;
// check blockchain, wait for confirmations...
return { success: true, fee, total: amount + fee };
} else if (method === 'bank_transfer') {
// 20 lines of bank transfer logic
// ...
}
// Every new payment method = modify this class
// This class grows FOREVER
// Testing requires testing ALL branches
// Violates Open/Closed Principle
}
}
Problems:
- Class grows larger with every new algorithm
- All algorithms mixed in one place -- hard to test individually
- Adding a new algorithm means modifying existing code (violates OCP)
- Can't reuse algorithms independently
The Solution: Strategy Pattern
Client Code
|
v
+--------------------+ <<interface>>
| Context | +------------------+
| | | Strategy |
| - strategy: Strategy+------>| + execute(data) |
| | +------------------+
| + setStrategy(s) | ^
| + executeStrategy() | |
+--------------------+ +---------+-----------+
| | |
+------+--+ +----+----+ +----+-----+
|Strategy A| |Strategy B| |Strategy C|
|+execute()| |+execute()| |+execute()|
+----------+ +----------+ +----------+
Implementation 1: Payment Processing Strategies
// ============================================
// STRATEGY INTERFACE
// ============================================
class PaymentStrategy {
/**
* @param {number} amount
* @param {object} details
* @returns {{ success: boolean, fee: number, total: number, receipt: string }}
*/
process(amount, details) {
throw new Error('PaymentStrategy.process() must be implemented');
}
validate(details) {
throw new Error('PaymentStrategy.validate() must be implemented');
}
getName() {
throw new Error('PaymentStrategy.getName() must be implemented');
}
}
// ============================================
// CONCRETE STRATEGIES
// ============================================
class CreditCardStrategy extends PaymentStrategy {
getName() { return 'Credit Card'; }
validate(details) {
const errors = [];
if (!details.cardNumber || details.cardNumber.length !== 16) {
errors.push('Invalid card number (must be 16 digits)');
}
if (!details.cvv || details.cvv.length !== 3) {
errors.push('Invalid CVV (must be 3 digits)');
}
if (!details.expiry) {
errors.push('Expiry date required');
}
return { valid: errors.length === 0, errors };
}
process(amount, details) {
const validation = this.validate(details);
if (!validation.valid) {
return { success: false, errors: validation.errors };
}
const fee = amount * 0.029 + 0.30; // 2.9% + $0.30
const total = amount + fee;
const lastFour = details.cardNumber.slice(-4);
console.log(`[CreditCard] Charged $${total.toFixed(2)} to card ****${lastFour}`);
return {
success: true,
fee: Number(fee.toFixed(2)),
total: Number(total.toFixed(2)),
receipt: `CC-${Date.now()}-${lastFour}`,
method: this.getName()
};
}
}
class PayPalStrategy extends PaymentStrategy {
getName() { return 'PayPal'; }
validate(details) {
const errors = [];
if (!details.email || !details.email.includes('@')) {
errors.push('Valid PayPal email required');
}
return { valid: errors.length === 0, errors };
}
process(amount, details) {
const validation = this.validate(details);
if (!validation.valid) {
return { success: false, errors: validation.errors };
}
const fee = amount * 0.035; // 3.5%
const total = amount + fee;
console.log(`[PayPal] Charged $${total.toFixed(2)} to ${details.email}`);
return {
success: true,
fee: Number(fee.toFixed(2)),
total: Number(total.toFixed(2)),
receipt: `PP-${Date.now()}`,
method: this.getName()
};
}
}
class CryptoStrategy extends PaymentStrategy {
getName() { return 'Cryptocurrency'; }
validate(details) {
const errors = [];
if (!details.walletAddress || details.walletAddress.length < 26) {
errors.push('Valid wallet address required');
}
if (!details.currency || !['BTC', 'ETH', 'USDC'].includes(details.currency)) {
errors.push('Supported currencies: BTC, ETH, USDC');
}
return { valid: errors.length === 0, errors };
}
process(amount, details) {
const validation = this.validate(details);
if (!validation.valid) {
return { success: false, errors: validation.errors };
}
const fee = amount * 0.01; // 1% (network fees)
const total = amount + fee;
console.log(
`[Crypto] Charged $${total.toFixed(2)} in ${details.currency} ` +
`from ${details.walletAddress.slice(0, 8)}...`
);
return {
success: true,
fee: Number(fee.toFixed(2)),
total: Number(total.toFixed(2)),
receipt: `CRYPTO-${Date.now()}-${details.currency}`,
method: this.getName(),
txHash: `0x${Math.random().toString(16).slice(2)}`
};
}
}
class BankTransferStrategy extends PaymentStrategy {
getName() { return 'Bank Transfer'; }
validate(details) {
const errors = [];
if (!details.routingNumber || details.routingNumber.length !== 9) {
errors.push('Valid 9-digit routing number required');
}
if (!details.accountNumber) {
errors.push('Account number required');
}
return { valid: errors.length === 0, errors };
}
process(amount, details) {
const validation = this.validate(details);
if (!validation.valid) {
return { success: false, errors: validation.errors };
}
const fee = 0; // No fee for bank transfers
const total = amount;
console.log(`[Bank] Initiated $${total.toFixed(2)} transfer (2-3 business days)`);
return {
success: true,
fee: 0,
total: Number(total.toFixed(2)),
receipt: `BANK-${Date.now()}`,
method: this.getName(),
estimatedArrival: '2-3 business days'
};
}
}
// ============================================
// CONTEXT
// ============================================
class PaymentProcessor {
constructor() {
this.strategy = null;
this.transactionHistory = [];
}
setStrategy(strategy) {
if (!(strategy instanceof PaymentStrategy)) {
throw new Error('Must provide a valid PaymentStrategy');
}
console.log(`[Processor] Strategy set to: ${strategy.getName()}`);
this.strategy = strategy;
return this;
}
processPayment(amount, details) {
if (!this.strategy) {
throw new Error('No payment strategy set. Call setStrategy() first.');
}
console.log(`\n--- Processing $${amount.toFixed(2)} via ${this.strategy.getName()} ---`);
const result = this.strategy.process(amount, details);
if (result.success) {
this.transactionHistory.push({
...result,
amount,
timestamp: new Date().toISOString()
});
}
return result;
}
getHistory() {
return [...this.transactionHistory];
}
}
// ============================================
// USAGE
// ============================================
const processor = new PaymentProcessor();
// Credit card payment
processor.setStrategy(new CreditCardStrategy());
processor.processPayment(100.00, {
cardNumber: '4111111111111111',
cvv: '123',
expiry: '12/25'
});
// Switch to PayPal at runtime
processor.setStrategy(new PayPalStrategy());
processor.processPayment(50.00, {
email: 'user@example.com'
});
// Switch to crypto
processor.setStrategy(new CryptoStrategy());
processor.processPayment(200.00, {
walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD38',
currency: 'ETH'
});
console.log('\nTransaction History:', processor.getHistory());
Implementation 2: Sorting Strategies
// ============================================
// SORTING STRATEGY INTERFACE
// ============================================
class SortStrategy {
sort(data) {
throw new Error('SortStrategy.sort() must be implemented');
}
getName() {
throw new Error('Must implement getName()');
}
getComplexity() {
throw new Error('Must implement getComplexity()');
}
}
// ============================================
// CONCRETE SORTING STRATEGIES
// ============================================
class BubbleSortStrategy extends SortStrategy {
getName() { return 'Bubble Sort'; }
getComplexity() { return { best: 'O(n)', average: 'O(n^2)', worst: 'O(n^2)', space: 'O(1)' }; }
sort(data, compareFn = (a, b) => a - b) {
const arr = [...data];
const n = arr.length;
let swaps = 0;
for (let i = 0; i < n - 1; i++) {
let swapped = false;
for (let j = 0; j < n - i - 1; j++) {
if (compareFn(arr[j], arr[j + 1]) > 0) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swapped = true;
swaps++;
}
}
if (!swapped) break; // Optimization: already sorted
}
console.log(`[${this.getName()}] Sorted ${n} elements with ${swaps} swaps`);
return arr;
}
}
class QuickSortStrategy extends SortStrategy {
getName() { return 'Quick Sort'; }
getComplexity() { return { best: 'O(n log n)', average: 'O(n log n)', worst: 'O(n^2)', space: 'O(log n)' }; }
sort(data, compareFn = (a, b) => a - b) {
const arr = [...data];
let comparisons = 0;
const quickSort = (arr, low, high) => {
if (low < high) {
const pivotIdx = partition(arr, low, high);
quickSort(arr, low, pivotIdx - 1);
quickSort(arr, pivotIdx + 1, high);
}
};
const partition = (arr, low, high) => {
const pivot = arr[high];
let i = low - 1;
for (let j = low; j < high; j++) {
comparisons++;
if (compareFn(arr[j], pivot) <= 0) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
return i + 1;
};
quickSort(arr, 0, arr.length - 1);
console.log(`[${this.getName()}] Sorted ${arr.length} elements with ${comparisons} comparisons`);
return arr;
}
}
class MergeSortStrategy extends SortStrategy {
getName() { return 'Merge Sort'; }
getComplexity() { return { best: 'O(n log n)', average: 'O(n log n)', worst: 'O(n log n)', space: 'O(n)' }; }
sort(data, compareFn = (a, b) => a - b) {
let mergeOps = 0;
const mergeSort = (arr) => {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
};
const merge = (left, right) => {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
mergeOps++;
if (compareFn(left[i], right[j]) <= 0) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return [...result, ...left.slice(i), ...right.slice(j)];
};
const sorted = mergeSort([...data]);
console.log(`[${this.getName()}] Sorted ${data.length} elements with ${mergeOps} merge ops`);
return sorted;
}
}
// ============================================
// ADAPTIVE CONTEXT: Auto-selects strategy
// ============================================
class SmartSorter {
constructor() {
this.strategies = {
bubble: new BubbleSortStrategy(),
quick: new QuickSortStrategy(),
merge: new MergeSortStrategy()
};
this.strategy = null;
}
setStrategy(name) {
this.strategy = this.strategies[name];
if (!this.strategy) {
throw new Error(`Unknown strategy: ${name}. Available: ${Object.keys(this.strategies)}`);
}
return this;
}
// Automatically choose the best strategy based on data characteristics
autoSelect(data) {
if (data.length < 20) {
this.strategy = this.strategies.bubble; // Simple for tiny arrays
} else if (data.length > 10000) {
this.strategy = this.strategies.merge; // Guaranteed O(n log n)
} else {
this.strategy = this.strategies.quick; // Good general purpose
}
console.log(`[SmartSorter] Auto-selected: ${this.strategy.getName()} for ${data.length} items`);
return this;
}
sort(data, compareFn) {
if (!this.strategy) this.autoSelect(data);
const start = performance.now();
const result = this.strategy.sort(data, compareFn);
const elapsed = (performance.now() - start).toFixed(2);
console.log(`[SmartSorter] Completed in ${elapsed}ms`);
console.log(`[SmartSorter] Complexity: ${JSON.stringify(this.strategy.getComplexity())}`);
return result;
}
}
// ============================================
// USAGE
// ============================================
const sorter = new SmartSorter();
const smallArray = [5, 3, 8, 1, 9, 2, 7];
const mediumArray = Array.from({ length: 100 }, () => Math.floor(Math.random() * 1000));
// Manual strategy selection
sorter.setStrategy('bubble').sort(smallArray);
sorter.setStrategy('quick').sort(mediumArray);
// Automatic strategy selection
sorter.autoSelect(smallArray).sort(smallArray);
sorter.autoSelect(mediumArray).sort(mediumArray);
Implementation 3: Compression Strategies
// ============================================
// COMPRESSION STRATEGIES
// ============================================
class CompressionStrategy {
compress(data) { throw new Error('Not implemented'); }
decompress(data) { throw new Error('Not implemented'); }
getName() { throw new Error('Not implemented'); }
}
class NoCompression extends CompressionStrategy {
getName() { return 'None'; }
compress(data) {
return {
data: data,
algorithm: this.getName(),
originalSize: data.length,
compressedSize: data.length,
ratio: 1.0
};
}
decompress(compressed) {
return compressed.data;
}
}
class RunLengthEncoding extends CompressionStrategy {
getName() { return 'RLE'; }
compress(data) {
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];
count = 1;
}
}
return {
data: compressed,
algorithm: this.getName(),
originalSize: data.length,
compressedSize: compressed.length,
ratio: (compressed.length / data.length).toFixed(3)
};
}
decompress(compressed) {
let result = '';
let numStr = '';
for (const char of compressed.data) {
if (/\d/.test(char)) {
numStr += char;
} else {
const count = numStr ? parseInt(numStr) : 1;
result += char.repeat(count);
numStr = '';
}
}
return result;
}
}
class HuffmanLikeEncoding extends CompressionStrategy {
getName() { return 'Huffman-like'; }
compress(data) {
// Build frequency map
const freq = {};
for (const char of data) {
freq[char] = (freq[char] || 0) + 1;
}
// Sort by frequency (most common = shortest code)
const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
const codeMap = {};
sorted.forEach(([char], i) => {
codeMap[char] = i.toString(36); // Use base36 for short codes
});
const encoded = [...data].map(c => codeMap[c]).join('|');
return {
data: encoded,
codeMap,
algorithm: this.getName(),
originalSize: data.length,
compressedSize: encoded.length,
ratio: (encoded.length / data.length).toFixed(3)
};
}
decompress(compressed) {
const reverseMap = {};
for (const [char, code] of Object.entries(compressed.codeMap)) {
reverseMap[code] = char;
}
return compressed.data.split('|').map(code => reverseMap[code]).join('');
}
}
// ============================================
// CONTEXT: File Compressor
// ============================================
class FileCompressor {
constructor() {
this.strategy = new NoCompression();
}
setStrategy(strategy) {
this.strategy = strategy;
console.log(`[Compressor] Using ${strategy.getName()} compression`);
return this;
}
compress(data) {
console.log(`\n--- Compressing ${data.length} characters ---`);
const result = this.strategy.compress(data);
console.log(
`[${result.algorithm}] ${result.originalSize} -> ${result.compressedSize} ` +
`(ratio: ${result.ratio})`
);
return result;
}
decompress(compressed) {
return this.strategy.decompress(compressed);
}
// Compare all strategies
benchmark(data, strategies) {
console.log(`\n=== Compression Benchmark (${data.length} chars) ===`);
console.log('------------------------------------------');
const results = [];
for (const strategy of strategies) {
const start = performance.now();
const compressed = strategy.compress(data);
const time = (performance.now() - start).toFixed(2);
// Verify decompression
const decompressed = strategy.decompress(compressed);
const valid = decompressed === data;
results.push({
name: strategy.getName(),
ratio: compressed.ratio,
time: `${time}ms`,
valid
});
console.log(
`${strategy.getName().padEnd(15)} | ` +
`Ratio: ${compressed.ratio} | ` +
`Time: ${time}ms | ` +
`Valid: ${valid}`
);
}
console.log('------------------------------------------');
return results;
}
}
// ============================================
// USAGE
// ============================================
const compressor = new FileCompressor();
const testData = 'AAAAAABBBCCCCCCDDDDDDDDDDEEEE';
compressor.setStrategy(new RunLengthEncoding());
const compressed = compressor.compress(testData);
const decompressed = compressor.decompress(compressed);
console.log('Decompressed:', decompressed);
console.log('Match:', decompressed === testData);
// Benchmark all strategies
compressor.benchmark(testData, [
new NoCompression(),
new RunLengthEncoding(),
new HuffmanLikeEncoding()
]);
Strategy vs If/Else Chains
+-------------------------------------------------------------------+
| |
| IF/ELSE APPROACH STRATEGY PATTERN |
| |
| function calculate(type) { class Calculator { |
| if (type === 'A') { setStrategy(strategy) { |
| // Algorithm A this.strategy = strategy; |
| } else if (type === 'B') { } |
| // Algorithm B calculate(data) { |
| } else if (type === 'C') { return this.strategy |
| // Algorithm C .execute(data); |
| } else if (type === 'D') { } |
| // Algorithm D } |
| } |
| } // Each algorithm in its own file |
| // Easy to add/remove/test |
| - One giant function |
| - Hard to test parts + Open/Closed Principle |
| - Violates SRP & OCP + Single Responsibility |
| - Growing complexity + Easy to test in isolation |
| + Runtime swappable |
+-------------------------------------------------------------------+
// STRATEGY PATTERN WITH FUNCTIONS (Lightweight JavaScript approach)
// In JS, strategies can simply be functions -- no class needed!
const strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
}
};
class Calculator {
constructor(strategy = 'add') {
this.setStrategy(strategy);
}
setStrategy(name) {
if (!strategies[name]) {
throw new Error(`Unknown strategy: ${name}`);
}
this.strategy = strategies[name];
this.strategyName = name;
return this;
}
execute(a, b) {
const result = this.strategy(a, b);
console.log(`${a} ${this.strategyName} ${b} = ${result}`);
return result;
}
}
const calc = new Calculator();
calc.setStrategy('add').execute(10, 5); // 15
calc.setStrategy('multiply').execute(10, 5); // 50
// Add new strategy WITHOUT modifying existing code
strategies.power = (a, b) => Math.pow(a, b);
calc.setStrategy('power').execute(2, 10); // 1024
Implementation 4: Validation Strategies
// ============================================
// FORM VALIDATION STRATEGIES
// ============================================
class ValidationStrategy {
validate(value) {
throw new Error('Must implement validate()');
}
}
class RequiredValidation extends ValidationStrategy {
validate(value) {
const isValid = value !== null && value !== undefined && value !== '';
return {
valid: isValid,
error: isValid ? null : 'This field is required'
};
}
}
class EmailValidation extends ValidationStrategy {
validate(value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(value);
return {
valid: isValid,
error: isValid ? null : 'Invalid email format'
};
}
}
class MinLengthValidation extends ValidationStrategy {
constructor(minLength) {
super();
this.minLength = minLength;
}
validate(value) {
const isValid = typeof value === 'string' && value.length >= this.minLength;
return {
valid: isValid,
error: isValid ? null : `Must be at least ${this.minLength} characters`
};
}
}
class PatternValidation extends ValidationStrategy {
constructor(pattern, message) {
super();
this.pattern = pattern;
this.message = message;
}
validate(value) {
const isValid = this.pattern.test(value);
return {
valid: isValid,
error: isValid ? null : this.message
};
}
}
class RangeValidation extends ValidationStrategy {
constructor(min, max) {
super();
this.min = min;
this.max = max;
}
validate(value) {
const num = Number(value);
const isValid = !isNaN(num) && num >= this.min && num <= this.max;
return {
valid: isValid,
error: isValid ? null : `Must be between ${this.min} and ${this.max}`
};
}
}
// ============================================
// FORM VALIDATOR (Context with multiple strategies per field)
// ============================================
class FormValidator {
constructor() {
this.fields = new Map(); // fieldName -> [strategies]
}
addField(name, ...strategies) {
this.fields.set(name, strategies);
return this;
}
validate(formData) {
const results = {
valid: true,
errors: {},
fieldCount: 0,
errorCount: 0
};
for (const [fieldName, strategies] of this.fields) {
results.fieldCount++;
const value = formData[fieldName];
const fieldErrors = [];
for (const strategy of strategies) {
const result = strategy.validate(value);
if (!result.valid) {
fieldErrors.push(result.error);
}
}
if (fieldErrors.length > 0) {
results.errors[fieldName] = fieldErrors;
results.errorCount += fieldErrors.length;
results.valid = false;
}
}
return results;
}
}
// ============================================
// USAGE
// ============================================
const registrationValidator = new FormValidator()
.addField('email',
new RequiredValidation(),
new EmailValidation()
)
.addField('password',
new RequiredValidation(),
new MinLengthValidation(8),
new PatternValidation(
/(?=.*[A-Z])(?=.*[0-9])/,
'Must contain at least one uppercase letter and one number'
)
)
.addField('age',
new RequiredValidation(),
new RangeValidation(13, 120)
);
// Valid form
console.log(registrationValidator.validate({
email: 'user@example.com',
password: 'SecurePass1',
age: 25
}));
// { valid: true, errors: {}, fieldCount: 3, errorCount: 0 }
// Invalid form
console.log(registrationValidator.validate({
email: 'not-an-email',
password: 'weak',
age: 5
}));
// { valid: false, errors: { email: [...], password: [...], age: [...] }, ... }
Strategy Pattern with Dependency Injection
// ============================================
// Strategy via constructor injection (most common in real apps)
// ============================================
class DataExporter {
constructor(formatStrategy, compressionStrategy) {
this.formatStrategy = formatStrategy;
this.compressionStrategy = compressionStrategy;
}
export(data) {
// Step 1: Format the data
const formatted = this.formatStrategy.format(data);
console.log(`Formatted as ${this.formatStrategy.getName()}: ${formatted.length} chars`);
// Step 2: Compress the formatted data
const compressed = this.compressionStrategy.compress(formatted);
console.log(`Compressed with ${this.compressionStrategy.getName()}: ratio ${compressed.ratio}`);
return compressed;
}
}
// Format strategies
const jsonFormat = {
getName: () => 'JSON',
format: (data) => JSON.stringify(data, null, 2)
};
const csvFormat = {
getName: () => 'CSV',
format: (data) => {
if (!Array.isArray(data) || data.length === 0) return '';
const headers = Object.keys(data[0]).join(',');
const rows = data.map(row => Object.values(row).join(','));
return [headers, ...rows].join('\n');
}
};
const xmlFormat = {
getName: () => 'XML',
format: (data) => {
const items = Array.isArray(data) ? data : [data];
const xmlItems = items.map(item => {
const fields = Object.entries(item)
.map(([k, v]) => ` <${k}>${v}</${k}>`)
.join('\n');
return ` <item>\n${fields}\n </item>`;
}).join('\n');
return `<?xml version="1.0"?>\n<data>\n${xmlItems}\n</data>`;
}
};
// Mix and match strategies freely
const data = [
{ name: 'Alice', age: 30, city: 'NYC' },
{ name: 'Bob', age: 25, city: 'LA' }
];
const exporter1 = new DataExporter(jsonFormat, new NoCompression());
const exporter2 = new DataExporter(csvFormat, new RunLengthEncoding());
const exporter3 = new DataExporter(xmlFormat, new NoCompression());
exporter1.export(data);
exporter2.export(data);
exporter3.export(data);
When to Use Strategy vs Alternatives
+-------------------------------------------------------------------+
| Pattern | Use When |
|--------------------+-----------------------------------------------|
| Strategy | Multiple algorithms for the same task, |
| | selected at runtime |
| | |
| Template Method | Same algorithm structure, different steps |
| | (inheritance-based) |
| | |
| State | Behavior changes based on internal state |
| | (transitions are automatic) |
| | |
| Command | Need to parameterize, queue, or undo actions |
| | |
| Simple if/else | Only 2-3 simple branches that won't grow |
+-------------------------------------------------------------------+
Key Takeaways
- Strategy eliminates conditional branches by encapsulating each algorithm in its own object/function
- In JavaScript, strategies can be plain functions -- you don't always need classes
- Strategies are swappable at runtime -- the context doesn't care which concrete strategy it uses
- Combine with Factory to create strategies based on configuration or user input
- Multiple strategies can compose -- e.g., format strategy + compression strategy
- Follows Open/Closed Principle -- add new strategies without modifying existing code
Explain-It Challenge
Your e-commerce platform needs to calculate shipping costs. Different strategies apply: flat rate, weight-based, distance-based, and free shipping (for orders over $100). The business wants to A/B test different strategies for different user segments. Design this system using Strategy pattern. How would you handle: (a) selecting strategy per user, (b) adding a new "express shipping" strategy, (c) combining strategies (e.g., weight + distance)?