Skip to main content

Strategy Pattern šŸŽÆ

Definition: 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.

šŸŽÆ Intent​

Enable selecting an algorithm's behavior at runtime by encapsulating algorithms in separate classes and making them interchangeable.

šŸ¤” Problem​

You have multiple ways of performing a task (like sorting, payment processing, or data validation) and you want to:

  • Switch between them at runtime
  • Add new algorithms without modifying existing code
  • Avoid massive if-else or switch statements

For example, an e-commerce app might support multiple payment methods: credit card, PayPal, cryptocurrency, etc. Without the Strategy pattern, you'd end up with a massive switch statement that violates the Open-Closed Principle.

šŸ’” Solution​

The Strategy pattern suggests extracting all different algorithms into separate classes called strategies. The original class (context) gets a field for storing a reference to one of the strategies and delegates the work to the linked strategy object.

šŸ—ļø Structure​

Context
ā”œā”€ā”€ strategy: Strategy
ā”œā”€ā”€ setStrategy(strategy: Strategy)
└── executeStrategy()

Strategy (interface)
└── execute()

ConcreteStrategyA implements Strategy
└── execute()

ConcreteStrategyB implements Strategy
└── execute()

ConcreteStrategyC implements Strategy
└── execute()

šŸ’» Code Example​

Basic Implementation​

// Strategy interface
class Strategy {
execute(data) {
throw new Error('execute() method must be implemented');
}
}

// Concrete Strategies
class QuickSort extends Strategy {
execute(data) {
console.log('Sorting using QuickSort algorithm');
return this.quickSort([...data]);
}

quickSort(arr) {
if (arr.length <= 1) return arr;

const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter((x) => x < pivot);
const middle = arr.filter((x) => x === pivot);
const right = arr.filter((x) => x > pivot);

return [...this.quickSort(left), ...middle, ...this.quickSort(right)];
}
}

class BubbleSort extends Strategy {
execute(data) {
console.log('Sorting using BubbleSort algorithm');
const arr = [...data];

for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
}

class MergeSort extends Strategy {
execute(data) {
console.log('Sorting using MergeSort algorithm');
return this.mergeSort([...data]);
}

mergeSort(arr) {
if (arr.length <= 1) return arr;

const mid = Math.floor(arr.length / 2);
const left = this.mergeSort(arr.slice(0, mid));
const right = this.mergeSort(arr.slice(mid));

return this.merge(left, right);
}

merge(left, right) {
const result = [];
let i = 0,
j = 0;

while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}

return result.concat(left.slice(i), right.slice(j));
}
}

// Context
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}

setStrategy(strategy) {
this.strategy = strategy;
}

sort(data) {
if (!this.strategy) {
throw new Error('No sorting strategy set');
}
return this.strategy.execute(data);
}
}

// Usage
const data = [64, 34, 25, 12, 22, 11, 90];
const sorter = new Sorter(new QuickSort());

console.log('Original:', data);
console.log('Sorted:', sorter.sort(data));

// Change strategy at runtime
sorter.setStrategy(new BubbleSort());
console.log('Sorted with Bubble Sort:', sorter.sort(data));

🌟 Real-World Examples​

1. Payment Processing System​

// Payment strategies
class PaymentStrategy {
pay(amount) {
throw new Error('pay() method must be implemented');
}
}

class CreditCardPayment extends PaymentStrategy {
constructor(cardNumber, expiryDate, cvv) {
super();
this.cardNumber = cardNumber;
this.expiryDate = expiryDate;
this.cvv = cvv;
}

pay(amount) {
console.log(`šŸ’³ Processing $${amount} via Credit Card`);
console.log(`Card: ****-****-****-${this.cardNumber.slice(-4)}`);

// Simulate payment processing
if (this.validateCard()) {
console.log('āœ… Payment successful!');
return { success: true, transactionId: `CC_${Date.now()}` };
}
return { success: false, error: 'Invalid card details' };
}

validateCard() {
return this.cardNumber.length === 16 && this.cvv.length === 3;
}
}

class PayPalPayment extends PaymentStrategy {
constructor(email, password) {
super();
this.email = email;
this.password = password;
}

pay(amount) {
console.log(`šŸ…æļø Processing $${amount} via PayPal`);
console.log(`Account: ${this.email}`);

if (this.authenticate()) {
console.log('āœ… Payment successful!');
return { success: true, transactionId: `PP_${Date.now()}` };
}
return { success: false, error: 'Authentication failed' };
}

authenticate() {
return this.email.includes('@') && this.password.length >= 6;
}
}

class CryptocurrencyPayment extends PaymentStrategy {
constructor(walletAddress, privateKey, currency) {
super();
this.walletAddress = walletAddress;
this.privateKey = privateKey;
this.currency = currency;
}

pay(amount) {
console.log(`₿ Processing $${amount} via ${this.currency}`);
console.log(`Wallet: ${this.walletAddress.slice(0, 6)}...${this.walletAddress.slice(-4)}`);

if (this.validateWallet()) {
console.log('āœ… Transaction broadcasted to blockchain!');
return { success: true, transactionId: `${this.currency}_${Date.now()}` };
}
return { success: false, error: 'Invalid wallet' };
}

validateWallet() {
return this.walletAddress.length >= 26;
}
}

// Shopping cart context
class ShoppingCart {
constructor() {
this.items = [];
this.paymentStrategy = null;
}

addItem(item, price) {
this.items.push({ item, price });
}

setPaymentStrategy(strategy) {
this.paymentStrategy = strategy;
}

getTotal() {
return this.items.reduce((total, item) => total + item.price, 0);
}

checkout() {
if (!this.paymentStrategy) {
console.log('āŒ Please select a payment method');
return false;
}

const total = this.getTotal();
console.log('\nšŸ›’ Order Summary:');
this.items.forEach((item) => {
console.log(` ${item.item}: $${item.price}`);
});
console.log(` Total: $${total}`);
console.log('─'.repeat(30));

return this.paymentStrategy.pay(total);
}
}

// Usage
const cart = new ShoppingCart();
cart.addItem('Laptop', 999.99);
cart.addItem('Mouse', 29.99);

// Try different payment methods
const creditCard = new CreditCardPayment('1234567812345678', '12/25', '123');
const paypal = new PayPalPayment('user@example.com', 'password123');
const bitcoin = new CryptocurrencyPayment(
'1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
'private_key',
'Bitcoin',
);

cart.setPaymentStrategy(creditCard);
cart.checkout();

cart.setPaymentStrategy(paypal);
cart.checkout();

cart.setPaymentStrategy(bitcoin);
cart.checkout();

2. Data Validation Strategies​

class ValidationStrategy {
validate(data) {
throw new Error('validate() method must be implemented');
}
}

class EmailValidation extends ValidationStrategy {
validate(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(email);

return {
isValid,
message: isValid ? 'Valid email' : 'Invalid email format',
};
}
}

class PasswordValidation extends ValidationStrategy {
constructor(minLength = 8) {
super();
this.minLength = minLength;
}

validate(password) {
const hasMinLength = password.length >= this.minLength;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>?]/.test(password);

const isValid = hasMinLength && hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChar;

const issues = [];
if (!hasMinLength) issues.push(`minimum ${this.minLength} characters`);
if (!hasUpperCase) issues.push('uppercase letter');
if (!hasLowerCase) issues.push('lowercase letter');
if (!hasNumbers) issues.push('number');
if (!hasSpecialChar) issues.push('special character');

return {
isValid,
message: isValid ? 'Strong password' : `Password must contain: ${issues.join(', ')}`,
};
}
}

class PhoneValidation extends ValidationStrategy {
validate(phone) {
const phoneRegex = /^\+?[\d\s\-\(\)]{10,}$/;
const isValid = phoneRegex.test(phone);

return {
isValid,
message: isValid ? 'Valid phone number' : 'Invalid phone number format',
};
}
}

class FormValidator {
constructor() {
this.strategies = new Map();
}

addValidation(field, strategy) {
this.strategies.set(field, strategy);
}

validate(formData) {
const results = {};

for (const [field, strategy] of this.strategies) {
if (formData.hasOwnProperty(field)) {
results[field] = strategy.validate(formData[field]);
}
}

return results;
}
}

// Usage
const validator = new FormValidator();
validator.addValidation('email', new EmailValidation());
validator.addValidation('password', new PasswordValidation(10));
validator.addValidation('phone', new PhoneValidation());

const formData = {
email: 'user@example.com',
password: 'Weak123',
phone: '+1-555-123-4567',
};

const results = validator.validate(formData);
console.log('Validation Results:', results);

3. Compression Strategies​

class CompressionStrategy {
compress(data) {
throw new Error('compress() method must be implemented');
}

decompress(data) {
throw new Error('decompress() method must be implemented');
}
}

class ZipCompression extends CompressionStrategy {
compress(data) {
console.log('šŸ—œļø Compressing with ZIP algorithm');
// Simulate ZIP compression
const compressed = `ZIP_COMPRESSED[${data.length}]`;
console.log(`Original: ${data.length} bytes → Compressed: ${compressed.length} bytes`);
return compressed;
}

decompress(compressedData) {
console.log('šŸ“¤ Decompressing ZIP data');
return compressedData.replace('ZIP_COMPRESSED[', '').replace(']', '');
}
}

class GzipCompression extends CompressionStrategy {
compress(data) {
console.log('šŸ—œļø Compressing with GZIP algorithm');
const compressed = `GZIP_COMPRESSED[${data.length}]`;
console.log(`Original: ${data.length} bytes → Compressed: ${compressed.length} bytes`);
return compressed;
}

decompress(compressedData) {
console.log('šŸ“¤ Decompressing GZIP data');
return compressedData.replace('GZIP_COMPRESSED[', '').replace(']', '');
}
}

class RarCompression extends CompressionStrategy {
compress(data) {
console.log('šŸ—œļø Compressing with RAR algorithm');
const compressed = `RAR_COMPRESSED[${data.length}]`;
console.log(`Original: ${data.length} bytes → Compressed: ${compressed.length} bytes`);
return compressed;
}

decompress(compressedData) {
console.log('šŸ“¤ Decompressing RAR data');
return compressedData.replace('RAR_COMPRESSED[', '').replace(']', '');
}
}

class FileCompressor {
constructor(strategy) {
this.strategy = strategy;
}

setStrategy(strategy) {
this.strategy = strategy;
}

compressFile(filename, data) {
console.log(`\nšŸ“ Processing file: ${filename}`);
return this.strategy.compress(data);
}

decompressFile(compressedData) {
return this.strategy.decompress(compressedData);
}
}

// Usage
const data = 'This is a sample file content that needs to be compressed...';
const compressor = new FileCompressor(new ZipCompression());

// Compress with ZIP
let compressed = compressor.compressFile('document.txt', data);

// Switch to GZIP
compressor.setStrategy(new GzipCompression());
compressed = compressor.compressFile('document.txt', data);

// Switch to RAR
compressor.setStrategy(new RarCompression());
compressed = compressor.compressFile('document.txt', data);

āœ… Pros​

  • Open/Closed Principle: You can introduce new strategies without changing existing code
  • Runtime Algorithm Selection: You can switch algorithms at runtime
  • Elimination of Conditionals: Replaces large switch statements or if-else chains
  • Isolation of Implementation Details: Each strategy encapsulates its algorithm
  • Code Reusability: Strategies can be reused across different contexts

āŒ Cons​

  • Increased Number of Classes: Can result in many small classes
  • Client Awareness: Clients must be aware of different strategies to choose the appropriate one
  • Communication Overhead: Context and strategies might need to exchange data
  • Strategy Interface Complexity: If strategies have very different requirements, the interface might become complex

šŸŽÆ When to Use​

  • Multiple Algorithm Variants: When you have multiple ways of performing a task
  • Runtime Selection: When you want to choose algorithms at runtime
  • Avoiding Conditionals: When you want to eliminate large conditional statements
  • Plugin Architecture: When building systems that support plugins or extensions
  • A/B Testing: When you need to test different implementations

šŸ”„ Common Variations​

1. Strategy with State​

class StatefulStrategy extends Strategy {
constructor() {
super();
this.state = {};
}

execute(data) {
// Use and modify internal state
this.state.executionCount = (this.state.executionCount || 0) + 1;
return this.process(data);
}

process(data) {
throw new Error('process() method must be implemented');
}
}

2. Strategy Factory​

class StrategyFactory {
static createStrategy(type) {
switch (type) {
case 'quick':
return new QuickSort();
case 'bubble':
return new BubbleSort();
case 'merge':
return new MergeSort();
default:
throw new Error(`Unknown strategy type: ${type}`);
}
}
}

// Usage
const strategy = StrategyFactory.createStrategy('quick');
const sorter = new Sorter(strategy);

3. Async Strategies​

class AsyncStrategy {
async execute(data) {
throw new Error('execute() method must be implemented');
}
}

class AsyncContext {
constructor(strategy) {
this.strategy = strategy;
}

async performOperation(data) {
return await this.strategy.execute(data);
}
}
  • State: Both patterns use composition and delegation, but Strategy focuses on algorithms while State focuses on behavior changes
  • Template Method: Both patterns deal with varying algorithms, but Strategy uses composition while Template Method uses inheritance
  • Bridge: Similar structure, but Bridge separates interface from implementation while Strategy makes algorithms interchangeable
  • Factory Method: Often used together to create appropriate strategies

šŸ“ˆ Performance Considerations​

  • Memory Usage: Each strategy creates a separate object
  • Strategy Switching Cost: Changing strategies has minimal overhead
  • Algorithm Complexity: Choose strategies based on data size and performance requirements

šŸ“š Further Reading​