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

  1. Strategy eliminates conditional branches by encapsulating each algorithm in its own object/function
  2. In JavaScript, strategies can be plain functions -- you don't always need classes
  3. Strategies are swappable at runtime -- the context doesn't care which concrete strategy it uses
  4. Combine with Factory to create strategies based on configuration or user input
  5. Multiple strategies can compose -- e.g., format strategy + compression strategy
  6. 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)?