Module: Design Patterns

Strategy

JavaScript Essentials: Design Patterns - Strategy

The Strategy pattern is a behavioral design pattern that lets you define a family of algorithms, encapsulate each one, and make them interchangeable. It allows the algorithm to vary independently from the clients that use it. In essence, it separates the what from the how.

Problem:

Imagine you need to calculate shipping costs for an e-commerce application. Shipping costs can vary based on the shipping method (e.g., Standard, Express, Overnight). Without a pattern, you might end up with a large, complex function with many if/else or switch statements to handle each shipping method. This leads to:

  • Code Bloat: The function becomes long and difficult to read.
  • Rigidity: Adding a new shipping method requires modifying existing code.
  • Maintainability Issues: Changes to one shipping method can potentially break others.

Solution: The Strategy Pattern

The Strategy pattern solves this by defining a separate class for each shipping method (each algorithm). These classes implement a common interface. The main shipping calculator class then holds a reference to a strategy object and delegates the cost calculation to it. You can easily switch between strategies at runtime.

Key Components:

  • Context: The class that uses the strategy. It doesn't implement the algorithm itself, but delegates it to the strategy. (e.g., ShippingCalculator)
  • Strategy Interface: Defines the common method(s) that all concrete strategies must implement. (e.g., ShippingStrategy)
  • Concrete Strategies: Implement the strategy interface, providing specific algorithms. (e.g., StandardShipping, ExpressShipping, OvernightShipping)

Implementation in JavaScript:

// 1. Strategy Interface
class ShippingStrategy {
  calculate(packageWeight, distance) {
    throw new Error("Method 'calculate()' must be implemented.");
  }
}

// 2. Concrete Strategies
class StandardShipping extends ShippingStrategy {
  calculate(packageWeight, distance) {
    return packageWeight * 0.1 + distance * 0.01;
  }
}

class ExpressShipping extends ShippingStrategy {
  calculate(packageWeight, distance) {
    return packageWeight * 0.3 + distance * 0.03;
  }
}

class OvernightShipping extends ShippingStrategy {
  calculate(packageWeight, distance) {
    return packageWeight * 0.5 + distance * 0.05;
  }
}

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

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

  calculateShippingCost(packageWeight, distance) {
    return this.strategy.calculate(packageWeight, distance);
  }
}

// Usage
const standardShipping = new StandardShipping();
const expressShipping = new ExpressShipping();
const overnightShipping = new OvernightShipping();

const calculator = new ShippingCalculator(standardShipping);

let cost = calculator.calculateShippingCost(5, 100);
console.log("Standard Shipping Cost:", cost); // Output: Standard Shipping Cost: 15

calculator.setStrategy(expressShipping);
cost = calculator.calculateShippingCost(5, 100);
console.log("Express Shipping Cost:", cost); // Output: Express Shipping Cost: 35

calculator.setStrategy(overnightShipping);
cost = calculator.calculateShippingCost(5, 100);
console.log("Overnight Shipping Cost:", cost); // Output: Overnight Shipping Cost: 55

Explanation:

  1. ShippingStrategy: This is the interface. It defines the calculate method, which all concrete strategies must implement.
  2. StandardShipping, ExpressShipping, OvernightShipping: These are the concrete strategies. Each class implements the calculate method with its specific logic for calculating shipping costs.
  3. ShippingCalculator: This is the context. It holds a reference to a ShippingStrategy object. The calculateShippingCost method delegates the actual calculation to the current strategy. The setStrategy method allows you to dynamically change the strategy at runtime.

Benefits of using the Strategy Pattern:

  • Flexibility: Easily add new algorithms (strategies) without modifying existing code.
  • Maintainability: Each algorithm is encapsulated in its own class, making it easier to understand and maintain.
  • Reusability: Strategies can be reused in different contexts.
  • Open/Closed Principle: The code is open for extension (adding new strategies) but closed for modification (existing code doesn't need to be changed).
  • Avoids Complex Conditional Logic: Replaces large if/else or switch statements with a more organized and maintainable structure.

When to use the Strategy Pattern:

  • You have multiple algorithms for a specific task.
  • You want to be able to switch between algorithms at runtime.
  • You want to avoid complex conditional logic.
  • You want to encapsulate algorithms in separate classes.
  • You want to adhere to the Open/Closed Principle.

Real-World Examples:

  • Payment Processing: Different payment methods (Credit Card, PayPal, Stripe) can be implemented as strategies.
  • Sorting Algorithms: Different sorting algorithms (Bubble Sort, Quick Sort, Merge Sort) can be implemented as strategies.
  • Compression Algorithms: Different compression algorithms (ZIP, GZIP, BZIP2) can be implemented as strategies.
  • Validation Rules: Different validation rules for form input can be implemented as strategies.

In conclusion, the Strategy pattern is a powerful tool for managing complexity and promoting flexibility in your JavaScript applications. By separating algorithms from the context that uses them, you can create more maintainable, reusable, and extensible code.