lesson

Strategy Selection and Comparison

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:

typescript
1const 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};
10
11// Direct strategy creation and assignment
12const calculator = new RateCalculator(new WeightBasedStrategy());
13
14// Switch based on business logic
15if (shipment.weight > 50) {
16 calculator.setStrategy(new WeightBasedStrategy());
17} else {
18 calculator.setStrategy(new FlatRateStrategy());
19}
20
21const 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:

typescript
1class StrategySelector {
2 static selectShippingStrategy(shipment: Shipment): RateStrategy {
3 // Rule 1: Heavy items use weight-based
4 if (shipment.weight > 50) {
5 return new WeightBasedStrategy();
6 }
7
8 // Rule 2: Large but light items use dimensional
9 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 }
14
15 // Rule 3: Cross-country uses zone-based
16 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 }
21
22 // Default: flat-rate for local, light shipments
23 return new FlatRateStrategy();
24 }
25}
26
27// Usage
28const 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:

typescript
1class OptimalStrategySelector {
2 private strategies: RateStrategy[] = [
3 new WeightBasedStrategy(),
4 new DimensionalStrategy(),
5 new FlatRateStrategy(),
6 new ZoneBasedStrategy()
7 ];
8
9 selectOptimalStrategy(shipment: Shipment): {
10 strategy: RateStrategy;
11 rate: number;
12 } {
13 let optimalStrategy: RateStrategy = this.strategies[0];
14 let lowestRate = Infinity;
15
16 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 }
28
29 return {
30 strategy: optimalStrategy,
31 rate: lowestRate
32 };
33 }
34}
35
36// Usage
37const 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

typescript
1class RateStrategyFactory {
2 static create(type: string, config?: any): RateStrategy {
3 switch (type.toLowerCase()) {
4 case 'weight':
5 return new WeightBasedStrategy(config?.ratePerPound);
6
7 case 'dimensional':
8 return new DimensionalStrategy(config?.ratePerPound);
9
10 case 'flat':
11 return new FlatRateStrategy(
12 config?.flatRate,
13 config?.maxWeight
14 );
15
16 case 'zone':
17 return new ZoneBasedStrategy();
18
19 default:
20 throw new Error(`Unknown strategy type: ${type}`);
21 }
22 }
23}
24
25// Usage
26const strategy = RateStrategyFactory.create('weight', { ratePerPound: 0.75 });
27const calculator = new RateCalculator(strategy);

Registry-Based Factory

More flexible factory using a registry pattern:

typescript
1type StrategyConstructor = new (...args: any[]) => RateStrategy;
2
3class StrategyRegistry {
4 private static strategies = new Map<string, StrategyConstructor>();
5
6 static register(name: string, constructor: StrategyConstructor): void {
7 this.strategies.set(name.toLowerCase(), constructor);
8 }
9
10 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 }
17
18 static getAvailableStrategies(): string[] {
19 return Array.from(this.strategies.keys());
20 }
21}
22
23// Register strategies
24StrategyRegistry.register('weight', WeightBasedStrategy);
25StrategyRegistry.register('dimensional', DimensionalStrategy);
26StrategyRegistry.register('flat', FlatRateStrategy);
27StrategyRegistry.register('zone', ZoneBasedStrategy);
28
29// Create strategies dynamically
30const 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

typescript
1interface StrategyConfig {
2 defaultStrategy: string;
3 rules: Array<{
4 condition: string;
5 strategy: string;
6 params?: any;
7 }>;
8}
9
10const 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};
29
30class ConfigurableStrategySelector {
31 constructor(private config: StrategyConfig) {}
32
33 select(shipment: Shipment): RateStrategy {
34 // Evaluate rules in order
35 for (const rule of this.config.rules) {
36 if (this.evaluateCondition(rule.condition, shipment)) {
37 return RateStrategyFactory.create(rule.strategy, rule.params);
38 }
39 }
40
41 // Return default strategy
42 return RateStrategyFactory.create(this.config.defaultStrategy);
43 }
44
45 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;
50
51 // Create evaluation context
52 const context = {
53 weight: shipment.weight,
54 dimensionalWeight,
55 distance: this.calculateDistance(shipment.origin, shipment.destination)
56 };
57
58 // 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 }
65
66 private calculateDistance(origin: string, destination: string): number {
67 // Simplified distance calculation
68 const originZip = parseInt(origin.substring(0, 3));
69 const destZip = parseInt(destination.substring(0, 3));
70 return Math.abs(originZip - destZip);
71 }
72}
73
74// Usage
75const 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

typescript
1interface StrategyRule {
2 id: string;
3 priority: number;
4 condition: string;
5 strategyType: string;
6 params: Record<string, any>;
7 active: boolean;
8}
9
10class DatabaseStrategySelector {
11 constructor(private db: Database) {}
12
13 async select(shipment: Shipment): Promise<RateStrategy> {
14 // Fetch active rules from database, sorted by priority
15 const rules = await this.db.query<StrategyRule>(`
16 SELECT * FROM strategy_rules
17 WHERE active = true
18 ORDER BY priority DESC
19 `);
20
21 // Evaluate rules
22 for (const rule of rules) {
23 if (this.evaluateCondition(rule.condition, shipment)) {
24 return RateStrategyFactory.create(rule.strategyType, rule.params);
25 }
26 }
27
28 // Default strategy
29 return new FlatRateStrategy();
30 }
31
32 private evaluateCondition(condition: string, shipment: Shipment): boolean {
33 // Implementation similar to ConfigurableStrategySelector
34 // ...
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:

typescript
1// Client selects sorting strategy
2const sorter = new Sorter(new QuickSortStrategy());
3sorter.sort(data);
4
5// Client can change strategy
6sorter.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:

typescript
1// Order changes its own state based on events
2const order = new Order();
3order.submit(); // Transitions from Draft → Submitted
4order.approve(); // Transitions from Submitted → Processing
5order.ship(); // Transitions from Processing → Shipped
6order.deliver(); // Transitions from Shipped → Delivered

Visual Comparison

1STRATEGY PATTERN STATE PATTERN
2┌─────────────┐ ┌─────────────┐
3│ Client │ │ Client │
4└──────┬──────┘ └──────┬──────┘
5 │ selects strategy │ triggers state change
6 ▼ ▼
7┌─────────────┐ ┌─────────────┐
8│ Context │ │ Context │
9│ - strategy │ │ - state │
10│ │ │ + request()│──┐
11└─────────────┘ └─────────────┘ │
12 │ │ │ changes
13 │ delegates │ delegates state
14 ▼ ▼ │
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:

typescript
1// Shipment uses State pattern for workflow
2class Shipment {
3 private state: ShipmentState;
4
5 processNextStep(): void {
6 this.state.process(this); // State pattern
7 }
8}
9
10// Each state uses Strategy pattern for rate calculation
11class ProcessingState implements ShipmentState {
12 process(shipment: Shipment): void {
13 // Use Strategy pattern to calculate rate
14 const calculator = new RateCalculator(new ZoneBasedStrategy());
15 const rate = calculator.calculateRate(shipment);
16
17 // Transition to next state
18 shipment.setState(new ShippedState());
19 }
20}

Best Practices for Strategy Selection

  1. Centralize Selection Logic: Use factories or selectors
  2. Make it Configurable: Use config files or database
  3. Provide Defaults: Always have a fallback strategy
  4. Document Rules: Clearly document selection criteria
  5. Test All Paths: Test each selection scenario
  6. Monitor Performance: Track which strategies are used
  7. Version Control: Track changes to selection rules
  8. 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!