Skip to Content
šŸ‘† We offer 1-on-1 classes as well check now

Strategy Pattern

The Strategy Pattern is a behavioral design pattern that enables you to define a family of algorithms, encapsulate each one as an object, and make them interchangeable. This allows the algorithm to vary independently from the clients that use it. It’s particularly useful when you have multiple algorithms for a specific task and you want to be able to switch between them at runtime without modifying the client code.

What is Strategy Pattern

The Strategy Pattern addresses situations where an algorithm (or a part of an algorithm) needs to be flexible and easily replaceable. Instead of embedding the logic of each algorithm directly within the client code (which leads to complex and tightly coupled code), the Strategy Pattern promotes loose coupling by extracting these algorithms into separate classes. These classes, known as ā€œstrategies,ā€ all implement a common interface.

The client (or ā€œcontextā€) holds a reference to a strategy object and delegates the execution of the algorithm to this object. The client can dynamically change the strategy object at runtime, effectively changing the algorithm being used.

In-depth Explanation:

The power of the Strategy Pattern lies in its ability to decouple the algorithm’s implementation from its usage. This decoupling offers several advantages:

  • Flexibility: New strategies can be added without modifying existing client code.
  • Maintainability: Changes to one strategy do not affect other strategies or the client.
  • Reusability: Strategies can be reused in different contexts.
  • Testability: Strategies can be tested independently.

Edge Cases:

  • Too many strategies: Overusing the Strategy Pattern can lead to an explosion of strategy classes, making the codebase harder to navigate. Carefully consider whether the added complexity is justified.
  • Simple algorithms: For very simple algorithms, the overhead of the Strategy Pattern might not be worth it. Consider using a simpler approach, such as a conditional statement.
  • Strategy selection logic: The client needs a mechanism to select the appropriate strategy. This selection logic can become complex if not handled carefully.

Performance Considerations:

  • Object creation overhead: Creating strategy objects can have a performance impact, especially if they are frequently created and destroyed. Consider using object pooling or caching to mitigate this.
  • Indirect method calls: Delegating the execution to a strategy object introduces an extra layer of indirection, which can slightly impact performance. However, the performance difference is usually negligible compared to the benefits of flexibility and maintainability.
  • Memory overhead: Storing multiple strategy objects can consume more memory compared to embedding the algorithm directly in the client.

Syntax and Usage

The Strategy Pattern typically involves the following components:

  1. Strategy Interface: Defines the interface that all concrete strategy classes must implement. This interface usually contains a single method that performs the algorithm’s core operation.

    class Strategy { public: virtual ~Strategy() = default; virtual int execute(int a, int b) = 0; };
  2. Concrete Strategies: Implement the Strategy interface and provide specific implementations of the algorithm.

    class AddStrategy : public Strategy { public: int execute(int a, int b) override { return a + b; } }; class SubtractStrategy : public Strategy { public: int execute(int a, int b) override { return a - b; } }; class MultiplyStrategy : public Strategy { public: int execute(int a, int b) override { return a * b; } };
  3. Context: Holds a reference to a Strategy object and uses it to perform the algorithm. The context is responsible for selecting and changing the strategy object.

    class Context { private: Strategy* strategy; public: Context(Strategy* strategy) : strategy(strategy) {} ~Context() { delete strategy; } void setStrategy(Strategy* strategy) { delete this->strategy; this->strategy = strategy; } int executeStrategy(int a, int b) { return strategy->execute(a, b); } };

Basic Example

This example demonstrates a calculator that can perform addition, subtraction, and multiplication using the Strategy Pattern.

#include <iostream> // Strategy Interface class Strategy { public: virtual ~Strategy() = default; virtual int execute(int a, int b) = 0; }; // Concrete Strategies class AddStrategy : public Strategy { public: int execute(int a, int b) override { return a + b; } }; class SubtractStrategy : public Strategy { public: int execute(int a, int b) override { return a - b; } }; class MultiplyStrategy : public Strategy { public: int execute(int a, int b) override { return a * b; } }; // Context class Context { private: Strategy* strategy; public: Context(Strategy* strategy) : strategy(strategy) {} ~Context() { delete strategy; } void setStrategy(Strategy* strategy) { delete this->strategy; this->strategy = strategy; } int executeStrategy(int a, int b) { return strategy->execute(a, b); } }; int main() { Context context(new AddStrategy()); std::cout << "10 + 5 = " << context.executeStrategy(10, 5) << std::endl; context.setStrategy(new SubtractStrategy()); std::cout << "10 - 5 = " << context.executeStrategy(10, 5) << std::endl; context.setStrategy(new MultiplyStrategy()); std::cout << "10 * 5 = " << context.executeStrategy(10, 5) << std::endl; return 0; }

Explanation:

  • The Strategy interface defines the execute method, which takes two integers as input and returns an integer.
  • The AddStrategy, SubtractStrategy, and MultiplyStrategy classes implement the Strategy interface and provide specific implementations for addition, subtraction, and multiplication, respectively.
  • The Context class holds a pointer to a Strategy object and delegates the execution of the algorithm to this object. The setStrategy method allows the client to change the strategy object at runtime.
  • In the main function, a Context object is created with an AddStrategy object. The executeStrategy method is called to perform addition. Then, the strategy is changed to SubtractStrategy and MultiplyStrategy, and the executeStrategy method is called again to perform subtraction and multiplication, respectively.

Advanced Example

Consider a scenario where you need to implement different sorting algorithms for a collection of data. You can use the Strategy Pattern to encapsulate each sorting algorithm as a separate strategy.

#include <iostream> #include <vector> #include <algorithm> // Strategy Interface class SortingStrategy { public: virtual ~SortingStrategy() = default; virtual void sort(std::vector<int>& data) = 0; }; // Concrete Strategies class BubbleSortStrategy : public SortingStrategy { public: void sort(std::vector<int>& data) override { std::cout << "Using Bubble Sort" << std::endl; for (size_t i = 0; i < data.size() - 1; ++i) { for (size_t j = 0; j < data.size() - i - 1; ++j) { if (data[j] > data[j + 1]) { std::swap(data[j], data[j + 1]); } } } } }; class QuickSortStrategy : public SortingStrategy { public: void sort(std::vector<int>& data) override { std::cout << "Using Quick Sort" << std::endl; std::sort(data.begin(), data.end()); // Using std::sort which is generally a highly optimized quicksort implementation. } }; // Context class DataSorter { private: SortingStrategy* strategy; public: DataSorter(SortingStrategy* strategy) : strategy(strategy) {} ~DataSorter() { delete strategy; } void setStrategy(SortingStrategy* strategy) { delete this->strategy; this->strategy = strategy; } void sortData(std::vector<int>& data) { strategy->sort(data); } }; int main() { std::vector<int> data = {5, 2, 8, 1, 9, 4}; DataSorter sorter(new BubbleSortStrategy()); sorter.sortData(data); std::cout << "Sorted data (Bubble Sort): "; for (int num : data) { std::cout << num << " "; } std::cout << std::endl; data = {5, 2, 8, 1, 9, 4}; // Reset data sorter.setStrategy(new QuickSortStrategy()); sorter.sortData(data); std::cout << "Sorted data (Quick Sort): "; for (int num : data) { std::cout << num << " "; } std::cout << std::endl; return 0; }

Common Use Cases

  • Payment processing: Different payment methods (credit card, PayPal, etc.) can be implemented as strategies.
  • Data validation: Different validation rules can be applied based on the data type or context.
  • Compression algorithms: Different compression algorithms (gzip, zip, etc.) can be used depending on the desired compression ratio and speed.

Best Practices

  • Favor composition over inheritance: The Strategy Pattern relies on composition (holding a reference to a strategy object) rather than inheritance. This promotes loose coupling and flexibility.
  • Use dependency injection: Inject the strategy object into the context through the constructor or a setter method. This makes the context more testable and configurable.
  • Avoid tight coupling between context and strategies: The context should only depend on the Strategy interface, not on specific concrete strategy classes.

Common Pitfalls

  • Overuse: Don’t use the Strategy Pattern for every algorithm. Consider whether the added complexity is justified.
  • Ignoring performance: Be aware of the performance implications of creating and switching between strategy objects.
  • Complex strategy selection logic: Keep the strategy selection logic as simple as possible.

Key Takeaways

  • The Strategy Pattern allows you to define a family of algorithms and make them interchangeable.
  • It promotes loose coupling between the client and the algorithms.
  • It improves flexibility, maintainability, and testability.
Last updated on