Strategy Selection and Pattern Comparison
Introduction
Selecting the right strategy at runtime is a critical aspect of the Strategy pattern. In this lesson, we'll explore different approaches to strategy selection, strategy factories, configuration-driven selection, and how Strategy differs from the similar State pattern.
Runtime Strategy Selection
1. Direct Selection
The simplest approach: create and assign strategies directly:
typescript1const shipment: Shipment = {2 id: 'SHP-001',3 weight: 25,4 length: 20,5 width: 16,6 height: 12,7 origin: '90210',8 destination: '10001'9};1011// Direct strategy creation and assignment12const calculator = new RateCalculator(new WeightBasedStrategy());1314// Switch based on business logic15if (shipment.weight > 50) {16 calculator.setStrategy(new WeightBasedStrategy());17} else {18 calculator.setStrategy(new FlatRateStrategy());19}2021const rate = calculator.calculateRate(shipment);
Pros:
- Simple and explicit
- Easy to understand
- Full control
Cons:
- Client must know about all strategies
- Scattered strategy selection logic
- Harder to test and maintain
2. Rule-Based Selection
Use business rules to select strategies automatically:
typescript1class StrategySelector {2 static selectShippingStrategy(shipment: Shipment): RateStrategy {3 // Rule 1: Heavy items use weight-based4 if (shipment.weight > 50) {5 return new WeightBasedStrategy();6 }78 // Rule 2: Large but light items use dimensional9 const volume = shipment.length * shipment.width * shipment.height;10 const dimensionalWeight = volume / 166;11 if (dimensionalWeight > shipment.weight * 1.5) {12 return new DimensionalStrategy();13 }1415 // Rule 3: Cross-country uses zone-based16 const originZone = parseInt(shipment.origin.substring(0, 1));17 const destZone = parseInt(shipment.destination.substring(0, 1));18 if (Math.abs(originZone - destZone) >= 3) {19 return new ZoneBasedStrategy();20 }2122 // Default: flat-rate for local, light shipments23 return new FlatRateStrategy();24 }25}2627// Usage28const strategy = StrategySelector.selectShippingStrategy(shipment);29const calculator = new RateCalculator(strategy);30const rate = calculator.calculateRate(shipment);
Benefits:
- Centralizes selection logic
- Encapsulates business rules
- Clients don't need strategy knowledge
- Easy to modify rules
3. Optimal Strategy Selection
Select the cheapest strategy for the customer:
typescript1class OptimalStrategySelector {2 private strategies: RateStrategy[] = [3 new WeightBasedStrategy(),4 new DimensionalStrategy(),5 new FlatRateStrategy(),6 new ZoneBasedStrategy()7 ];89 selectOptimalStrategy(shipment: Shipment): {10 strategy: RateStrategy;11 rate: number;12 } {13 let optimalStrategy: RateStrategy = this.strategies[0];14 let lowestRate = Infinity;1516 for (const strategy of this.strategies) {17 try {18 const rate = strategy.calculate(shipment);19 if (rate < lowestRate) {20 lowestRate = rate;21 optimalStrategy = strategy;22 }23 } catch (error) {24 // Skip strategies that throw errors (e.g., weight limits)25 continue;26 }27 }2829 return {30 strategy: optimalStrategy,31 rate: lowestRate32 };33 }34}3536// Usage37const selector = new OptimalStrategySelector();38const { strategy, rate } = selector.selectOptimalStrategy(shipment);39console.log(`Best rate: $${rate} using ${strategy.getName()}`);
Use Cases:
- Price optimization
- Customer satisfaction
- Competitive pricing
- Marketing promotions
Strategy Factories
Factories create strategies based on configuration or input parameters.
Simple Strategy Factory
typescript1class RateStrategyFactory {2 static create(type: string, config?: any): RateStrategy {3 switch (type.toLowerCase()) {4 case 'weight':5 return new WeightBasedStrategy(config?.ratePerPound);67 case 'dimensional':8 return new DimensionalStrategy(config?.ratePerPound);910 case 'flat':11 return new FlatRateStrategy(12 config?.flatRate,13 config?.maxWeight14 );1516 case 'zone':17 return new ZoneBasedStrategy();1819 default:20 throw new Error(`Unknown strategy type: ${type}`);21 }22 }23}2425// Usage26const strategy = RateStrategyFactory.create('weight', { ratePerPound: 0.75 });27const calculator = new RateCalculator(strategy);
Registry-Based Factory
More flexible factory using a registry pattern:
typescript1type StrategyConstructor = new (...args: any[]) => RateStrategy;23class StrategyRegistry {4 private static strategies = new Map<string, StrategyConstructor>();56 static register(name: string, constructor: StrategyConstructor): void {7 this.strategies.set(name.toLowerCase(), constructor);8 }910 static create(name: string, ...args: any[]): RateStrategy {11 const Constructor = this.strategies.get(name.toLowerCase());12 if (!Constructor) {13 throw new Error(`Strategy '${name}' not registered`);14 }15 return new Constructor(...args);16 }1718 static getAvailableStrategies(): string[] {19 return Array.from(this.strategies.keys());20 }21}2223// Register strategies24StrategyRegistry.register('weight', WeightBasedStrategy);25StrategyRegistry.register('dimensional', DimensionalStrategy);26StrategyRegistry.register('flat', FlatRateStrategy);27StrategyRegistry.register('zone', ZoneBasedStrategy);2829// Create strategies dynamically30const strategy = StrategyRegistry.create('weight', 0.75);
Benefits:
- Extensible without modifying factory
- Supports plugin architecture
- Easy to add custom strategies
- Discoverable (can list available strategies)
Configuration-Driven Strategy Selection
JSON Configuration
typescript1interface StrategyConfig {2 defaultStrategy: string;3 rules: Array<{4 condition: string;5 strategy: string;6 params?: any;7 }>;8}910const config: StrategyConfig = {11 defaultStrategy: 'flat',12 rules: [13 {14 condition: 'weight > 50',15 strategy: 'weight',16 params: { ratePerPound: 0.60 }17 },18 {19 condition: 'dimensionalWeight > weight * 1.5',20 strategy: 'dimensional',21 params: { ratePerPound: 0.70 }22 },23 {24 condition: 'distance > 1000',25 strategy: 'zone'26 }27 ]28};2930class ConfigurableStrategySelector {31 constructor(private config: StrategyConfig) {}3233 select(shipment: Shipment): RateStrategy {34 // Evaluate rules in order35 for (const rule of this.config.rules) {36 if (this.evaluateCondition(rule.condition, shipment)) {37 return RateStrategyFactory.create(rule.strategy, rule.params);38 }39 }4041 // Return default strategy42 return RateStrategyFactory.create(this.config.defaultStrategy);43 }4445 private evaluateCondition(condition: string, shipment: Shipment): boolean {46 // Simple expression evaluator (in production, use a proper parser)47 try {48 const dimensionalWeight =49 (shipment.length * shipment.width * shipment.height) / 166;5051 // Create evaluation context52 const context = {53 weight: shipment.weight,54 dimensionalWeight,55 distance: this.calculateDistance(shipment.origin, shipment.destination)56 };5758 // Evaluate condition (simplified - use a library like expr-eval in production)59 return new Function(...Object.keys(context), `return ${condition}`)60 (...Object.values(context));61 } catch {62 return false;63 }64 }6566 private calculateDistance(origin: string, destination: string): number {67 // Simplified distance calculation68 const originZip = parseInt(origin.substring(0, 3));69 const destZip = parseInt(destination.substring(0, 3));70 return Math.abs(originZip - destZip);71 }72}7374// Usage75const selector = new ConfigurableStrategySelector(config);76const strategy = selector.select(shipment);
Benefits:
- No code changes for new rules
- Business users can modify rules
- A/B testing different strategies
- Environment-specific configurations
Database-Driven Selection
typescript1interface StrategyRule {2 id: string;3 priority: number;4 condition: string;5 strategyType: string;6 params: Record<string, any>;7 active: boolean;8}910class DatabaseStrategySelector {11 constructor(private db: Database) {}1213 async select(shipment: Shipment): Promise<RateStrategy> {14 // Fetch active rules from database, sorted by priority15 const rules = await this.db.query<StrategyRule>(`16 SELECT * FROM strategy_rules17 WHERE active = true18 ORDER BY priority DESC19 `);2021 // Evaluate rules22 for (const rule of rules) {23 if (this.evaluateCondition(rule.condition, shipment)) {24 return RateStrategyFactory.create(rule.strategyType, rule.params);25 }26 }2728 // Default strategy29 return new FlatRateStrategy();30 }3132 private evaluateCondition(condition: string, shipment: Shipment): boolean {33 // Implementation similar to ConfigurableStrategySelector34 // ...35 }36}
Strategy vs State Pattern Comparison
The Strategy and State patterns have similar structures but different intents.
Strategy Pattern
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
Key Characteristics:
- Client typically chooses the strategy
- Strategies are independent and interchangeable
- Context doesn't change strategies on its own
- Strategies are usually stateless
Example:
typescript1// Client selects sorting strategy2const sorter = new Sorter(new QuickSortStrategy());3sorter.sort(data);45// Client can change strategy6sorter.setStrategy(new MergeSortStrategy());7sorter.sort(moreData);
State Pattern
Intent: Allow an object to alter its behavior when its internal state changes.
Key Characteristics:
- Object changes its own state
- States can transition to other states
- Context behavior changes with state
- States may be stateful
Example:
typescript1// Order changes its own state based on events2const order = new Order();3order.submit(); // Transitions from Draft → Submitted4order.approve(); // Transitions from Submitted → Processing5order.ship(); // Transitions from Processing → Shipped6order.deliver(); // Transitions from Shipped → Delivered
Visual Comparison
1STRATEGY PATTERN STATE PATTERN2┌─────────────┐ ┌─────────────┐3│ Client │ │ Client │4└──────┬──────┘ └──────┬──────┘5 │ selects strategy │ triggers state change6 ▼ ▼7┌─────────────┐ ┌─────────────┐8│ Context │ │ Context │9│ - strategy │ │ - state │10│ │ │ + request()│──┐11└─────────────┘ └─────────────┘ │12 │ │ │ changes13 │ delegates │ delegates state14 ▼ ▼ │15┌─────────────┐ ┌─────────────┐ │16│ Strategy │ │ State │◄─┘17└─────────────┘ │ + handle() │18 △ └─────────────┘19 │ △20 implements implements
When to Use Each
Use Strategy when:
- You need different algorithms for the same task
- Client controls which algorithm to use
- Algorithms are independent
- You want to eliminate conditionals
Use State when:
- Object behavior changes based on internal state
- State transitions are well-defined
- Current state affects available operations
- You want to eliminate state-based conditionals
Hybrid Approach
Sometimes, you can combine both patterns:
typescript1// Shipment uses State pattern for workflow2class Shipment {3 private state: ShipmentState;45 processNextStep(): void {6 this.state.process(this); // State pattern7 }8}910// Each state uses Strategy pattern for rate calculation11class ProcessingState implements ShipmentState {12 process(shipment: Shipment): void {13 // Use Strategy pattern to calculate rate14 const calculator = new RateCalculator(new ZoneBasedStrategy());15 const rate = calculator.calculateRate(shipment);1617 // Transition to next state18 shipment.setState(new ShippedState());19 }20}
Best Practices for Strategy Selection
- Centralize Selection Logic: Use factories or selectors
- Make it Configurable: Use config files or database
- Provide Defaults: Always have a fallback strategy
- Document Rules: Clearly document selection criteria
- Test All Paths: Test each selection scenario
- Monitor Performance: Track which strategies are used
- Version Control: Track changes to selection rules
- Validate Strategies: Ensure selected strategy can handle input
Summary
- Strategy Selection can be direct, rule-based, or optimal
- Factories provide flexible strategy creation
- Configuration-Driven selection enables runtime changes
- Strategy Pattern focuses on interchangeable algorithms
- State Pattern focuses on state-dependent behavior
- Choose the right pattern based on your use case
In the next workshop, you'll implement a complete rate calculator with multiple strategies and selection logic!