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:
-
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; }; -
Concrete Strategies: Implement the
Strategyinterface 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; } }; -
Context: Holds a reference to a
Strategyobject 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
Strategyinterface defines theexecutemethod, which takes two integers as input and returns an integer. - The
AddStrategy,SubtractStrategy, andMultiplyStrategyclasses implement theStrategyinterface and provide specific implementations for addition, subtraction, and multiplication, respectively. - The
Contextclass holds a pointer to aStrategyobject and delegates the execution of the algorithm to this object. ThesetStrategymethod allows the client to change the strategy object at runtime. - In the
mainfunction, aContextobject is created with anAddStrategyobject. TheexecuteStrategymethod is called to perform addition. Then, the strategy is changed toSubtractStrategyandMultiplyStrategy, and theexecuteStrategymethod 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
Strategyinterface, 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.