Skip to content

Strategy Pattern

Overview

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it. This pattern promotes the Open/Closed Principle by allowing you to add new algorithms without changing the existing code.

Intent

  • Define a family of algorithms and encapsulate each one
  • Make algorithms interchangeable within that family
  • Let the algorithm vary independently from clients that use it
  • Eliminate conditional statements for algorithm selection

Problem

When you have multiple ways to perform a task (different algorithms), you often end up with large conditional statements (if-else or switch-case) that select which algorithm to use. This approach leads to:

  • Code that's difficult to maintain and extend
  • Violation of the Open/Closed Principle (must modify code to add algorithms)
  • Algorithm logic scattered throughout the code
  • Difficulty testing algorithms independently
  • Tight coupling between the context and algorithm implementations

Solution

Extract each algorithm into separate classes with a common interface. The context maintains a reference to a strategy object and delegates the algorithm execution to it. Clients can switch strategies at runtime, and new strategies can be added without modifying existing code.

Structure

Class Diagram

Strategy Pattern Diagram 0

Components

  1. Strategy (Interface)

    • Declares an interface common to all supported algorithms
    • Context uses this interface to call the algorithm
  2. ConcreteStrategy

    • Implements the algorithm using the Strategy interface
    • Each concrete strategy provides a different implementation
  3. Context

    • Maintains a reference to a Strategy object
    • May define an interface to let Strategy access its data
    • Delegates algorithm execution to the strategy

Implementation Examples

Example 1: Sorting Strategies

Different sorting algorithms for different data characteristics.

Strategy Pattern Diagram 1

Algorithm Selection Criteria:

Algorithm Best For Time Complexity Space Complexity
Bubble Sort Small datasets (< 10 items) O(n²) O(1)
Quick Sort General purpose, large datasets O(n log n) avg O(log n)
Merge Sort Stable sort needed, linked lists O(n log n) O(n)

Usage:

1
2
3
4
5
6
7
8
9
std::vector<int> data = {64, 34, 25, 12, 22, 11, 90};

// Use Bubble Sort for small dataset
Sorter sorter(std::make_shared<BubbleSort>());
sorter.sort(data);

// Switch to Quick Sort for larger dataset
sorter.setStrategy(std::make_shared<QuickSort>());
sorter.sort(data);

Example 2: Payment Processing

Different payment methods in an e-commerce system.

Strategy Pattern Diagram 2

Usage:

ShoppingCart cart;
cart.addItem("Laptop", 999.99);
cart.addItem("Mouse", 29.99);

// Customer chooses payment method
auto creditCard = std::make_shared<CreditCardPayment>(
    "1234567890123456", "123", "12/25");
cart.setPaymentMethod(creditCard);
cart.checkout();

// Different customer, different payment
cart.addItem("Book", 19.99);
auto paypal = std::make_shared<PayPalPayment>("user@example.com");
cart.setPaymentMethod(paypal);
cart.checkout();

Example 3: Compression Strategies

Different compression algorithms for files.

Strategy Pattern Diagram 3

Compression Trade-offs:

  • ZIP: Good balance, wide compatibility
  • RAR: Better compression ratio, proprietary
  • GZIP: Fast, commonly used for web content

Usage:

1
2
3
4
5
6
7
8
9
std::string data = "Large file content...";

// Compress with ZIP
FileCompressor compressor(std::make_shared<ZipCompression>());
std::string compressed = compressor.compressFile("file.zip", data);

// Switch to GZIP for web content
compressor.setCompressionStrategy(std::make_shared<GzipCompression>());
compressed = compressor.compressFile("file.gz", data);

Example 4: Navigation Routes

Different routing strategies for navigation apps.

Strategy Pattern Diagram 4

Route Characteristics:

Mode Speed Cost Environment Health
Car Fast High (fuel) High impact Low
Public Transit Medium Low Low impact Low
Bicycle Medium Very low Very low High
Walking Slow Free Zero Very high

Usage:

Navigator navigator(std::make_shared<CarRoute>());
navigator.buildRoute("Home", "Office");
// Route: Use highways, 30km, 45 minutes

// Switch to public transport
navigator.setRouteStrategy(std::make_shared<PublicTransportRoute>());
navigator.buildRoute("Home", "Office");
// Route: Bus + Train, $3.50, 65 minutes

// Eco-friendly option
navigator.setRouteStrategy(std::make_shared<BicycleRoute>());
navigator.buildRoute("Home", "Office");
// Route: Bike lanes, 22km, 90 minutes

Example 5: Discount Pricing

Different discount strategies for pricing.

Strategy Pattern Diagram 5

Usage:

PricingCalculator calculator;

// Regular price
calculator.calculatePrice(50.0, 2);  // $100.00

// Percentage discount (20% off)
calculator.setDiscountStrategy(std::make_shared<PercentageDiscount>(20.0));
calculator.calculatePrice(50.0, 2);  // $80.00

// Fixed discount ($15 off)
calculator.setDiscountStrategy(std::make_shared<FixedAmountDiscount>(15.0));
calculator.calculatePrice(50.0, 2);  // $85.00

// BOGO (Buy One Get One Free)
calculator.setDiscountStrategy(std::make_shared<BuyOneGetOneFree>());
calculator.calculatePrice(50.0, 2);  // $50.00 (50% off)

Sequence Diagram

Strategy Pattern Diagram 6

Real-World Applications

1. Sorting Algorithms

Standard Library Example:

1
2
3
4
5
6
std::vector<int> data = {5, 2, 8, 1, 9};

// Different sorting strategies
std::sort(data.begin(), data.end());  // Default (IntroSort)
std::stable_sort(data.begin(), data.end());  // Stable sort
std::partial_sort(data.begin(), data.begin() + 3, data.end());  // Partial sort

Custom Comparators:

1
2
3
4
5
// Sort by different criteria
std::sort(data.begin(), data.end(), std::greater<int>());  // Descending
std::sort(data.begin(), data.end(), [](int a, int b) {
    return std::abs(a) < std::abs(b);  // By absolute value
});

2. Payment Gateways

E-commerce Platforms: - Credit cards: Stripe, Square, Authorize.net - Digital wallets: PayPal, Apple Pay, Google Pay - Bank transfers: ACH, Wire transfer - Cryptocurrency: Bitcoin, Ethereum - Buy now, pay later: Klarna, Affirm

Each payment method has different: - Processing fees - Processing times - Security requirements - Geographic availability

3. Data Serialization

Format Selection:

class DataExporter {
    std::shared_ptr<ExportStrategy> strategy_;
public:
    void exportData(const Data& data) {
        strategy_->export(data);
    }
};

// Different export formats
exporter.setStrategy(std::make_shared<JSONExport>());
exporter.setStrategy(std::make_shared<XMLExport>());
exporter.setStrategy(std::make_shared<CSVExport>());
exporter.setStrategy(std::make_shared<PDFExport>());

4. Image Processing

Filter Strategies: - Blur: Gaussian blur, motion blur, radial blur - Sharpen: Unsharp mask, high-pass filter - Color: Grayscale, sepia, HDR - Artistic: Oil painting, cartoon, sketch

1
2
3
4
ImageProcessor processor;
processor.setFilter(std::make_shared<GaussianBlur>(5.0));
processor.setFilter(std::make_shared<SepiaFilter>());
processor.setFilter(std::make_shared<EdgeDetection>());

5. Authentication Methods

Login Strategies:

class AuthenticationService {
    std::shared_ptr<AuthStrategy> strategy_;
public:
    bool authenticate(const Credentials& creds) {
        return strategy_->verify(creds);
    }
};

// Different authentication methods
service.setStrategy(std::make_shared<PasswordAuth>());
service.setStrategy(std::make_shared<OAuthAuth>());
service.setStrategy(std::make_shared<BiometricAuth>());
service.setStrategy(std::make_shared<TwoFactorAuth>());

6. Caching Strategies

Cache Eviction Policies: - LRU (Least Recently Used) - LFU (Least Frequently Used) - FIFO (First In, First Out) - Random replacement

class Cache {
    std::shared_ptr<EvictionStrategy> strategy_;
public:
    void put(Key key, Value value) {
        if (isFull()) {
            strategy_->evict();
        }
        // ... add to cache
    }
};

7. Rendering Engines

Graphics Rendering: - Rasterization: Fast, real-time games - Ray tracing: Realistic lighting, movies - Path tracing: Physically accurate, architectural visualization

class Renderer {
    std::shared_ptr<RenderStrategy> strategy_;
public:
    void render(const Scene& scene) {
        strategy_->render(scene);
    }
};

// Performance vs quality trade-off
renderer.setStrategy(std::make_shared<FastRasterizer>());  // 60+ FPS
renderer.setStrategy(std::make_shared<RayTracer>());       // Realistic

8. Text Search Algorithms

Search Strategies: - Brute force: Simple, O(n*m) - Boyer-Moore: Fast for large texts - Rabin-Karp: Multiple pattern search - Regex: Complex pattern matching

1
2
3
4
5
6
7
class TextSearcher {
    std::shared_ptr<SearchStrategy> strategy_;
public:
    int find(const std::string& text, const std::string& pattern) {
        return strategy_->search(text, pattern);
    }
};

Design Considerations

Advantages

Open/Closed Principle: Add new strategies without modifying existing code.

1
2
3
4
5
6
7
8
9
// Adding a new strategy doesn't affect existing code
class HeapSort : public SortStrategy {
    void sort(std::vector<int>& data) override {
        // Heap sort implementation
    }
};

// Use it without changing Sorter class
sorter.setStrategy(std::make_shared<HeapSort>());

Runtime Algorithm Selection: Switch algorithms dynamically based on conditions.

1
2
3
4
5
6
7
if (dataSize < 10) {
    sorter.setStrategy(std::make_shared<BubbleSort>());
} else if (dataSize < 1000) {
    sorter.setStrategy(std::make_shared<QuickSort>());
} else {
    sorter.setStrategy(std::make_shared<MergeSort>());
}

Eliminates Conditional Statements: Replace complex conditionals with polymorphism.

// Without Strategy Pattern
void processPayment(PaymentType type, double amount) {
    if (type == CREDIT_CARD) {
        // Credit card logic
    } else if (type == PAYPAL) {
        // PayPal logic
    } else if (type == CRYPTO) {
        // Crypto logic
    }
}

// With Strategy Pattern
void processPayment(std::shared_ptr<PaymentStrategy> strategy, double amount) {
    strategy->pay(amount);
}

Testability: Each strategy can be tested independently.

1
2
3
4
5
6
7
// Easy to unit test individual strategies
TEST(BubbleSortTest, SortsCorrectly) {
    BubbleSort strategy;
    std::vector<int> data = {5, 2, 8, 1};
    strategy.sort(data);
    EXPECT_EQ(data, std::vector<int>({1, 2, 5, 8}));
}

Reusability: Strategies can be reused across different contexts.

Disadvantages

Increased Number of Objects: Each algorithm requires a separate class.

Client Awareness: Clients must be aware of different strategies to select the appropriate one.

1
2
3
4
5
6
// Client needs to know about all strategies
if (userPreference == "fast") {
    strategy = std::make_shared<QuickSort>();
} else if (userPreference == "stable") {
    strategy = std::make_shared<MergeSort>();
}

Communication Overhead: Strategy may need access to context data.

1
2
3
4
5
6
7
8
// Strategy might need context information
class ComplexStrategy {
    void execute(Context* context) {
        auto data = context->getData();
        auto config = context->getConfiguration();
        // Use context data...
    }
};

Overkill for Simple Cases: If you have only 2-3 algorithms that rarely change, simpler approaches may suffice.

Strategy vs State Pattern

Aspect Strategy Pattern State Pattern
Intent Select algorithm family Change behavior based on state
Who decides Client selects strategy Context or states decide transitions
Independence Strategies are independent States often know about other states
Transitions No automatic transitions States can transition to other states
Focus Algorithm selection State management
Usage Different ways to do same thing Different behavior for different states

Strategy Example:

1
2
3
4
5
6
// Client explicitly chooses
context.setStrategy(new ConcreteStrategyA());
context.execute();  // Uses strategy A

context.setStrategy(new ConcreteStrategyB());
context.execute();  // Uses strategy B

State Example:

1
2
3
// State transitions internally
context.request();  // May transition from StateA to StateB
context.request();  // May transition from StateB to StateC

When to Use Strategy Pattern

Use Strategy Pattern when:

  • You have multiple algorithms for a specific task
  • You want to switch algorithms at runtime
  • You have many related classes that differ only in behavior
  • An algorithm uses data that clients shouldn't know about
  • A class has multiple conditional statements that select behavior

Good Candidates: - Sorting algorithms - Compression algorithms - Payment processing - Route planning - Data validation - Rendering methods - Search algorithms

Avoid Strategy Pattern when:

  • You have only one or two simple algorithms
  • Algorithms never change
  • Simple conditional logic is sufficient
  • The overhead of extra classes is unjustified
  • All clients use the same algorithm

Use Simpler Alternatives: - Function pointers or lambdas for simple cases - Template methods for similar algorithms - Simple if-else for 2-3 alternatives

Best Practices

1. Use Smart Pointers

1
2
3
4
5
6
7
class Context {
    std::shared_ptr<Strategy> strategy_;  // Automatic memory management
public:
    void setStrategy(std::shared_ptr<Strategy> strategy) {
        strategy_ = strategy;
    }
};

2. Provide Default Strategy

class Context {
    std::shared_ptr<Strategy> strategy_;
public:
    Context() : strategy_(std::make_shared<DefaultStrategy>()) {}

    void execute() {
        if (strategy_) {
            strategy_->execute();
        }
    }
};

3. Factory for Strategy Creation

class StrategyFactory {
public:
    static std::shared_ptr<SortStrategy> create(const std::string& type) {
        if (type == "quick") return std::make_shared<QuickSort>();
        if (type == "merge") return std::make_shared<MergeSort>();
        if (type == "bubble") return std::make_shared<BubbleSort>();
        return std::make_shared<QuickSort>();  // Default
    }
};

// Usage
auto strategy = StrategyFactory::create("quick");
sorter.setStrategy(strategy);

4. Strategy with Context Data

class Strategy {
public:
    virtual void execute(Context* context) = 0;
};

class ConcreteStrategy : public Strategy {
public:
    void execute(Context* context) override {
        auto data = context->getData();
        // Use context data
    }
};

5. Fluent Interface

class Context {
public:
    Context& setStrategy(std::shared_ptr<Strategy> strategy) {
        strategy_ = strategy;
        return *this;
    }

    Context& execute() {
        strategy_->execute();
        return *this;
    }
};

// Usage
context.setStrategy(strategyA).execute()
       .setStrategy(strategyB).execute();
  • State: Similar structure but different intent. State manages state transitions; Strategy selects algorithms.
  • Template Method: Defines algorithm skeleton in base class with steps overridden by subclasses. Strategy encapsulates entire algorithms.
  • Factory Method: Can be used to create strategies.
  • Flyweight: Strategies can be implemented as flyweights if they have no state.
  • Decorator: Both patterns involve composing objects, but Decorator adds responsibilities while Strategy changes algorithms.

Summary

The Strategy Pattern provides a clean way to: - Encapsulate algorithms in separate classes - Switch algorithms at runtime based on conditions - Eliminate conditional statements that select behavior - Follow the Open/Closed Principle by adding new strategies without modifying existing code

Use it when you have multiple algorithms and need flexibility to switch between them. It's particularly valuable in scenarios like sorting, payment processing, compression, navigation, and any situation where "how" something is done can vary while "what" is done remains the same.

The pattern shines in systems where algorithm selection depends on runtime conditions, user preferences, or data characteristics. However, for simple cases with only a couple of algorithms, simpler approaches like function pointers or basic conditionals may be more appropriate.