JavaScript Strategy Pattern: Interchangeable Algorithms
The Strategy Pattern is a behavioral design pattern that allows developers to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. This article explains how the Strategy Pattern works in JavaScript, demonstrates both object-oriented and functional implementations, and illustrates how decoupling business logic from algorithm execution creates modular, scalable, and maintainable applications.
Understanding the Strategy Pattern
In software development, applications often need to execute different
variants of an algorithm based on user input, system state, or business
rules. Without a structured pattern, this typically results in bulky
if-else or switch statements that violate the
Open/Closed Principle (software entities should be open for extension,
but closed for modification).
The Strategy Pattern resolves this issue by dividing the logic into three components:
- Context: The entity that maintains a reference to a strategy and delegates algorithmic work to it.
- Strategy: The common interface or signature that all algorithms adhere to.
- Concrete Strategies: The individual implementations of the specific algorithms.
Because JavaScript treats functions as first-class citizens, the Strategy Pattern can be implemented using standard classes or lightweight functional objects.
Implementing the Strategy Pattern in JavaScript
Consider a common scenario: calculating shipping costs across different carriers.
Object-Oriented Approach
In an object-oriented style, each shipping carrier acts as a concrete
strategy class implementing a shared method name
(calculate).
// Concrete Strategies
class FedExStrategy {
calculate(packageDetails) {
return packageDetails.weight * 2.45 + packageDetails.distance * 0.15;
}
}
class UPSStrategy {
calculate(packageDetails) {
return packageDetails.weight * 2.10 + packageDetails.distance * 0.18;
}
}
class PostalStrategy {
calculate(packageDetails) {
return packageDetails.weight * 1.50 + packageDetails.distance * 0.10;
}
}
// Context
class ShippingContext {
constructor() {
this.strategy = null;
}
setStrategy(strategy) {
this.strategy = strategy;
}
calculateShipping(packageDetails) {
if (!this.strategy) {
throw new Error("Shipping strategy has not been set.");
}
return this.strategy.calculate(packageDetails);
}
}
// Usage
const packageDetails = { weight: 10, distance: 100 };
const shipping = new ShippingContext();
shipping.setStrategy(new FedExStrategy());
console.log(`FedEx: $${shipping.calculateShipping(packageDetails)}`);
shipping.setStrategy(new UPSStrategy());
console.log(`UPS: $${shipping.calculateShipping(packageDetails)}`);Functional Approach
JavaScript allows for a more concise functional implementation using an object map containing pure functions.
// Strategies defined as a dictionary of functions
const shippingStrategies = {
fedex: ({ weight, distance }) => weight * 2.45 + distance * 0.15,
ups: ({ weight, distance }) => weight * 2.10 + distance * 0.18,
postal: ({ weight, distance }) => weight * 1.50 + distance * 0.10,
};
// Context function
function calculateShipping(carrier, packageDetails) {
const strategy = shippingStrategies[carrier];
if (!strategy) {
throw new Error(`Unsupported carrier: ${carrier}`);
}
return strategy(packageDetails);
}
// Usage
const details = { weight: 10, distance: 100 };
console.log(calculateShipping('fedex', details));
console.log(calculateShipping('postal', details));How Interchangeability is Achieved
The Strategy Pattern enables dynamic interchangeability through three core mechanisms:
- Consistent Interfaces: Every strategy accepts the same arguments and returns the same output structure, allowing the context to invoke them identically without knowing their internal mechanics.
- Runtime Polymorphism: The context object does not hardcode which algorithm to run. Instead, it accepts any valid strategy assignment at runtime, adapting immediately to new conditions or configurations.
- Separation of Concerns: Adding a new algorithm requires writing a new strategy file or function without altering existing calculations or context code, reducing regression risks.
By leveraging the Strategy Pattern, JavaScript applications can seamlessly swap calculation engines, validation routines, authentication methods, or rendering logic while keeping the codebase clean, testable, and decoupled.