Design Principle
Strategy Pattern
Learn the Strategy pattern: define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients.
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.
What is the Strategy Pattern?
Strategy Pattern provides:
- Algorithm encapsulation: Encapsulate algorithms in separate classes
- Interchangeability: Algorithms are interchangeable
- Runtime selection: Select algorithm at runtime
- Open/Closed Principle: Open for extension, closed for modification
Use cases:
- Sorting algorithms
- Payment processing
- Compression algorithms
- Validation strategies
- Discount calculations
Structure
Context
└─ strategy: Strategy
└─ execute() (uses strategy.algorithm())
Strategy (interface)
└─ algorithm()
ConcreteStrategyA (implements Strategy)
└─ algorithm()
ConcreteStrategyB (implements Strategy)
└─ algorithm()
Examples
Payment Strategy
// Strategy interface
interface PaymentStrategy {
pay(amount: number): void;
}
// Concrete strategies
class CreditCardStrategy implements PaymentStrategy {
constructor(private cardNumber: string, private cvv: string) {}
pay(amount: number): void {
console.log(`Paid $${amount} using credit card ending in ${this.cardNumber.slice(-4)}`);
}
}
class PayPalStrategy implements PaymentStrategy {
constructor(private email: string) {}
pay(amount: number): void {
console.log(`Paid $${amount} using PayPal account ${this.email}`);
}
}
class BankTransferStrategy implements PaymentStrategy {
constructor(private accountNumber: string) {}
pay(amount: number): void {
console.log(`Paid $${amount} using bank transfer to account ${this.accountNumber}`);
}
}
// Context
class PaymentProcessor {
private strategy: PaymentStrategy;
constructor(strategy: PaymentStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
processPayment(amount: number): void {
this.strategy.pay(amount);
}
}
// Usage
const processor = new PaymentProcessor(new CreditCardStrategy("1234567890", "123"));
processor.processPayment(100);
processor.setStrategy(new PayPalStrategy("user@example.com"));
processor.processPayment(200);
Sorting Strategy
// Strategy interface
interface SortStrategy {
sort(data: number[]): number[];
}
// Concrete strategies
class BubbleSortStrategy implements SortStrategy {
sort(data: number[]): number[] {
console.log("Sorting using bubble sort");
const arr = [...data];
for (let i = 0; i < arr.length; 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 QuickSortStrategy implements SortStrategy {
sort(data: number[]): number[] {
console.log("Sorting using quick sort");
return this.quickSort([...data]);
}
private quickSort(arr: number[]): number[] {
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 MergeSortStrategy implements SortStrategy {
sort(data: number[]): number[] {
console.log("Sorting using merge sort");
return this.mergeSort([...data]);
}
private mergeSort(arr: number[]): number[] {
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);
}
private merge(left: number[], right: number[]): number[] {
const result: number[] = [];
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)).concat(right.slice(j));
}
}
// Context
class Sorter {
private strategy: SortStrategy;
constructor(strategy: SortStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: SortStrategy): void {
this.strategy = strategy;
}
sort(data: number[]): number[] {
return this.strategy.sort(data);
}
}
// Usage
const data = [64, 34, 25, 12, 22, 11, 90];
const sorter = new Sorter(new BubbleSortStrategy());
console.log(sorter.sort(data));
sorter.setStrategy(new QuickSortStrategy());
console.log(sorter.sort(data));
sorter.setStrategy(new MergeSortStrategy());
console.log(sorter.sort(data));
Discount Strategy
// Strategy interface
interface DiscountStrategy {
calculateDiscount(price: number): number;
}
// Concrete strategies
class NoDiscountStrategy implements DiscountStrategy {
calculateDiscount(price: number): number {
return 0;
}
}
class PercentageDiscountStrategy implements DiscountStrategy {
constructor(private percentage: number) {}
calculateDiscount(price: number): number {
return price * (this.percentage / 100);
}
}
class FixedDiscountStrategy implements DiscountStrategy {
constructor(private amount: number) {}
calculateDiscount(price: number): number {
return Math.min(this.amount, price);
}
}
class SeasonalDiscountStrategy implements DiscountStrategy {
calculateDiscount(price: number): number {
const month = new Date().getMonth();
// 20% discount in December
if (month === 11) {
return price * 0.2;
}
return 0;
}
}
// Context
class PriceCalculator {
private discountStrategy: DiscountStrategy;
constructor(discountStrategy: DiscountStrategy) {
this.discountStrategy = discountStrategy;
}
setDiscountStrategy(strategy: DiscountStrategy): void {
this.discountStrategy = strategy;
}
calculateFinalPrice(price: number): number {
const discount = this.discountStrategy.calculateDiscount(price);
return price - discount;
}
}
// Usage
const calculator = new PriceCalculator(new NoDiscountStrategy());
console.log(calculator.calculateFinalPrice(100)); // 100
calculator.setDiscountStrategy(new PercentageDiscountStrategy(10));
console.log(calculator.calculateFinalPrice(100)); // 90
calculator.setDiscountStrategy(new FixedDiscountStrategy(20));
console.log(calculator.calculateFinalPrice(100)); // 80
calculator.setDiscountStrategy(new SeasonalDiscountStrategy());
console.log(calculator.calculateFinalPrice(100)); // 80 (if December)
Common Pitfalls
- Too many strategies: Can become complex. Fix: Group related strategies
- Strategy selection: Hard to choose strategy. Fix: Use factory pattern
- Context bloat: Context becomes too large. Fix: Keep context simple
- Not using strategy: Using if-else instead. Fix: Use strategy pattern for multiple algorithms
Interview Questions
Beginner
Q: What is the Strategy pattern and when would you use it?
A:
Strategy Pattern defines a family of algorithms, encapsulates each, and makes them interchangeable.
Key characteristics:
- Algorithm encapsulation: Each algorithm in separate class
- Interchangeability: Algorithms are interchangeable
- Runtime selection: Select algorithm at runtime
- Open/Closed: Open for extension, closed for modification
Example:
interface SortStrategy {
sort(data: number[]): number[];
}
class QuickSort implements SortStrategy {
sort(data: number[]): number[] { /* ... */ }
}
class MergeSort implements SortStrategy {
sort(data: number[]): number[] { /* ... */ }
}
class Sorter {
private strategy: SortStrategy;
sort(data: number[]): number[] {
return this.strategy.sort(data);
}
}
Use cases:
- Multiple algorithms: Different ways to do same thing
- Runtime selection: Choose algorithm at runtime
- Payment methods: Credit card, PayPal, etc.
- Sorting algorithms: Quick sort, merge sort, etc.
Benefits:
- Flexibility: Easy to add new algorithms
- Testability: Test each strategy independently
- Maintainability: Changes isolated to strategy classes
Intermediate
Q: Explain how the Strategy pattern works. How does it differ from if-else or switch statements?
A:
Strategy Pattern Structure:
1. Strategy Interface:
interface Strategy {
algorithm(): void;
}
2. Concrete Strategies:
class StrategyA implements Strategy {
algorithm(): void {
// Algorithm A
}
}
class StrategyB implements Strategy {
algorithm(): void {
// Algorithm B
}
}
3. Context:
class Context {
private strategy: Strategy;
setStrategy(strategy: Strategy): void {
this.strategy = strategy;
}
execute(): void {
this.strategy.algorithm();
}
}
Difference from If-Else:
If-Else (Not Strategy):
function process(type: string) {
if (type === "A") {
// Algorithm A
} else if (type === "B") {
// Algorithm B
}
}
Problems:
- Not extensible: Need to modify function for new types
- Violates Open/Closed: Closed for extension
- Hard to test: Can't test algorithms independently
Strategy Pattern:
class Context {
private strategy: Strategy;
execute(): void {
this.strategy.algorithm(); // Extensible, testable
}
}
Benefits:
- Extensible: Add new strategies without modifying context
- Testable: Test each strategy independently
- Maintainable: Changes isolated to strategy classes
Senior
Q: Design a strategy system for a recommendation engine that uses different recommendation algorithms (collaborative filtering, content-based, hybrid). Handle algorithm selection based on user preferences, data availability, and performance requirements.
A:
// Strategy interface
interface RecommendationStrategy {
recommend(userId: string, limit: number): Promise<Recommendation[]>;
getAlgorithmName(): string;
getPerformanceMetrics(): PerformanceMetrics;
}
// Collaborative filtering strategy
class CollaborativeFilteringStrategy implements RecommendationStrategy {
async recommend(userId: string, limit: number): Promise<Recommendation[]> {
// Find similar users
const similarUsers = await this.findSimilarUsers(userId);
// Get items liked by similar users
const recommendations = await this.getItemsFromSimilarUsers(similarUsers);
// Rank and return top N
return this.rankRecommendations(recommendations).slice(0, limit);
}
getAlgorithmName(): string {
return "Collaborative Filtering";
}
getPerformanceMetrics(): PerformanceMetrics {
return {
accuracy: 0.85,
latency: 200, // ms
requiresUserData: true
};
}
private async findSimilarUsers(userId: string): Promise<string[]> {
// Find users with similar preferences
return [];
}
private async getItemsFromSimilarUsers(users: string[]): Promise<Recommendation[]> {
// Get items from similar users
return [];
}
private rankRecommendations(items: Recommendation[]): Recommendation[] {
// Rank by relevance
return items.sort((a, b) => b.score - a.score);
}
}
// Content-based strategy
class ContentBasedStrategy implements RecommendationStrategy {
async recommend(userId: string, limit: number): Promise<Recommendation[]> {
// Get user preferences
const preferences = await this.getUserPreferences(userId);
// Find items with similar features
const recommendations = await this.findSimilarItems(preferences);
return recommendations.slice(0, limit);
}
getAlgorithmName(): string {
return "Content-Based";
}
getPerformanceMetrics(): PerformanceMetrics {
return {
accuracy: 0.75,
latency: 150,
requiresUserData: false
};
}
private async getUserPreferences(userId: string): Promise<Preferences> {
// Get user's past preferences
return {};
}
private async findSimilarItems(preferences: Preferences): Promise<Recommendation[]> {
// Find items matching preferences
return [];
}
}
// Hybrid strategy
class HybridStrategy implements RecommendationStrategy {
private strategies: RecommendationStrategy[];
constructor() {
this.strategies = [
new CollaborativeFilteringStrategy(),
new ContentBasedStrategy()
];
}
async recommend(userId: string, limit: number): Promise<Recommendation[]> {
// Get recommendations from all strategies
const allRecommendations = await Promise.all(
this.strategies.map(s => s.recommend(userId, limit * 2))
);
// Merge and deduplicate
const merged = this.mergeRecommendations(allRecommendations);
// Re-rank
return this.rerank(merged).slice(0, limit);
}
getAlgorithmName(): string {
return "Hybrid";
}
getPerformanceMetrics(): PerformanceMetrics {
return {
accuracy: 0.90,
latency: 300,
requiresUserData: true
};
}
private mergeRecommendations(recommendations: Recommendation[][]): Recommendation[] {
// Merge and deduplicate
const map = new Map<string, Recommendation>();
recommendations.forEach(list => {
list.forEach(rec => {
const existing = map.get(rec.itemId);
if (!existing || existing.score < rec.score) {
map.set(rec.itemId, rec);
}
});
});
return Array.from(map.values());
}
private rerank(recommendations: Recommendation[]): Recommendation[] {
// Re-rank merged recommendations
return recommendations.sort((a, b) => b.score - a.score);
}
}
// Strategy selector
class RecommendationStrategySelector {
selectStrategy(context: RecommendationContext): RecommendationStrategy {
// Select based on context
if (context.hasUserData && context.performanceRequirement === "high") {
return new CollaborativeFilteringStrategy();
} else if (!context.hasUserData) {
return new ContentBasedStrategy();
} else {
return new HybridStrategy();
}
}
}
// Context
class RecommendationEngine {
private strategy: RecommendationStrategy;
constructor(strategy: RecommendationStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: RecommendationStrategy): void {
this.strategy = strategy;
}
async getRecommendations(userId: string, limit: number): Promise<Recommendation[]> {
return await this.strategy.recommend(userId, limit);
}
getPerformanceMetrics(): PerformanceMetrics {
return this.strategy.getPerformanceMetrics();
}
}
// Usage
const selector = new RecommendationStrategySelector();
const context = {
hasUserData: true,
performanceRequirement: "high"
};
const strategy = selector.selectStrategy(context);
const engine = new RecommendationEngine(strategy);
const recommendations = await engine.getRecommendations("user123", 10);
Features:
- Multiple strategies: Different recommendation algorithms
- Strategy selection: Select based on context
- Performance metrics: Track algorithm performance
- Hybrid approach: Combine multiple strategies
Key Takeaways
- Strategy pattern: Encapsulates algorithms, makes them interchangeable
- Runtime selection: Select algorithm at runtime
- Open/Closed Principle: Open for extension, closed for modification
- Use cases: Multiple algorithms, payment methods, sorting, validation
- Benefits: Flexibility, testability, maintainability
- Best practices: Use factory for selection, keep context simple, group related strategies
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.