Design Patterns: Strategy

The Strategy design pattern allows the client to choose the appropriate algorithm at runtime, providing flexibility and promoting a clean separation of concerns.

Design Patterns: Strategy

Problem

In software design, there are scenarios where a class should have the ability to alter its behavior when its internal state changes. However, using conditional statements to manage these variations can lead to code that is difficult to maintain and extend.

Solution

The problem can be addressed by a behavioral design pattern named Strategy. The pattern defines a family of algorithms, encapsulating each one, and making them interchangeable. This pattern allows the client to choose the appropriate algorithm at runtime, providing flexibility and promoting a clean separation of concerns.

To implement the pattern, create an interface or abstract class that declares the common methods to be used by all concrete strategies. Implement concrete strategies, each representing a specific algorithm or behavior.

class diagram

Create a context class that contains a reference to the strategy interface. This class delegates the algorithm execution to the concrete strategy at runtime. In the client code, instantiate the context class with the desired strategy.

sequence diagram

This allows the client to switch between different strategies dynamically.

Pros

  • Flexibility and Extensibility: Strategies can be added, removed, or replaced without affecting the client code, providing flexibility and making the system easily extensible.
  • Encapsulation: Each strategy encapsulates a specific behavior, promoting a clean separation of concerns and making it easier to manage and maintain the code.
  • Promotes Code Reusability: Concrete strategies can be reused in different contexts, reducing redundant code and promoting a more modular and reusable design.
  • Improved Testability: Each strategy can be tested independently, simplifying the testing process and ensuring that changes in one strategy do not impact others.
  • Dynamic Behavior: Strategies can be switched at runtime, allowing dynamic changes in behavior without modifying the context class.

Cons

  • Increased Number of Classes: The pattern may lead to an increased number of classes, especially if there are many strategies. This can make the codebase more complex, although this is often a trade-off for increased flexibility.
  • Potential Overhead: The overhead of managing multiple strategy classes and the interaction with the context might be perceived as unnecessary in simpler scenarios where behaviors rarely change.
  • Client's Responsibility: The client needs to be aware of the available strategies and choose the appropriate one. In some cases, this responsibility might be better placed elsewhere if the client is not well-suited for making such decisions.
  • Complexity for Simple Cases: For simple scenarios with only a few fixed behaviors, applying the Strategy Pattern might be overkill and can introduce unnecessary complexity.

Implementation

In the implementation section, we explore the practical application of the Strategy Pattern. Our focus will be on the key components: the Strategy Interface, Concrete Strategies, and the Context Class, demonstrated in code snippets.

JAVA

interface Strategy {
    void algorithm();
}

class FirstStrategy implements Strategy {
    public void algorithm() {
        System.out.println("Executing first strategy");
    }
}

class SecondStrategy implements Strategy {
    public void algorithm() {
        System.out.println("Executing second strategy");
    }
}

class ThirdStrategy implements Strategy {
    public void algorithm() {
        System.out.println("Executing third strategy");
    }
}

The provided code defines a Strategy interface with a single method, algorithm(). Three concrete classes, FirstStrategy, SecondStrategy, and ThirdStrategy, implement this interface, each providing a distinct implementation of the algorithm() method. These classes represent different strategies or algorithms that can be dynamically chosen and executed. The algorithm() method in each class prints a message indicating the execution of the specific strategy.

class StrategyContext {
    private Strategy strategy;

    public StrategyContext(Strategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        strategy.algorithm();
    }
}

The provided code snippet defines a StrategyContext class that encapsulates the Strategy design pattern. The class contains a private member variable strategy of type Strategy, initialized through the constructor. Additionally, it provides a setStrategy method, allowing the dynamic modification of the strategy associated with the context at runtime. The execute method is responsible for triggering the execution of the current strategy's algorithm. By using the setStrategy method, clients can change the strategy associated with the context, enabling the StrategyContext to adapt to different algorithms dynamically.

StrategyContext context = new StrategyContext(new FirstStrategy());
context.execute();

context.setStrategy(new SecondStrategy());
context.execute();

context.setStrategy(new ThirdStrategy());
context.execute();

The provided code snippet demonstrates the use of the Strategy. It begins by creating instances of three different strategies: FirstStrategy, SecondStrategy, and ThirdStrategy. Then, a StrategyContext is instantiated with the initial strategy set to strategy1. The execute() method is called on the context, invoking the algorithm associated with FirstStrategy. Subsequently, the strategy is dynamically changed using the setStrategy method. The context's strategy is set to strategy2, and the execute() method is called again, executing the algorithm associated with SecondStrategy. Lastly, the strategy is changed once more to strategy3, and the execute() method is invoked, triggering the algorithm associated with the third strategy instance of ThirdStrategy. This code illustrates the flexibility of the Strategy design pattern, allowing the dynamic switching of algorithms at runtime within a single context.

C++

class Strategy {
public:
  virtual ~Strategy() = default;
  virtual void algorithm() const = 0;
};

class FirstStrategy : public Strategy {
public:
  void algorithm() const override final { std::cout << "Executing first strategy" << std::endl; }
};

class SecondStrategy : public Strategy {
public:
  void algorithm() const override final { std::cout << "Executing second strategy" << std::endl; }
};

class ThirdStrategy : public Strategy {
public:
  void algorithm() const override final { std::cout << "Executing third strategy" << std::endl; }
};

The above C++ code snippet establishes a foundation for the Strategy design pattern. The Strategy class defines an interface with a pure virtual method algorithm(), ensuring that all concrete strategies adhere to a common structure. The virtual destructor allows for proper cleanup in derived classes. Three concrete strategy classes, namely FirstStrategy, SecondStrategy, and ThirdStrategy, inherit from the Strategy base class. Each of these concrete classes overrides the algorithm() method, providing specific implementations that print messages indicating the execution of their respective strategies. The use of the final keyword in the derived classes ensures that the algorithm() implementations cannot be further overridden in any subsequent classes.

class StrategyContext {
private:
  std::unique_ptr<Strategy> strategy;

public:
  StrategyContext(Strategy* strategy) : strategy(strategy) {}

  void set_strategy(Strategy* strategy) { this->strategy.reset(strategy); }

  void execute() const { strategy->algorithm(); }
};

The StrategyContext class contains a private member variable strategy, which is a std::unique_ptr pointing to a Strategy object. The constructor initializes this pointer with a provided strategy. The class also includes a set_strategy method, allowing the dynamic modification of the strategy associated with the context by resetting the unique_ptr. The execute method triggers the execution of the current strategy's algorithm. This implementation ensures proper ownership and lifetime management of the strategy objects, utilizing std::unique_ptr to handle memory deallocation automatically when the strategy changes.

    StrategyContext context(new FirstStrategy);
    context.execute();

    context.set_strategy(new SecondStrategy);
    context.execute();

    context.set_strategy(new ThirdStrategy);
    context.execute();

The above code demonstrates the application of the Strategy design pattern within a StrategyContext. Initially, an instance of StrategyContext is created, and its constructor is passed a dynamically allocated instance of FirstStrategy. The execute method is then called, triggering the execution of the algorithm associated with the first strategy. Subsequently, the strategy is dynamically changed using the set_strategy method, and the execute method is invoked again, this time with an instance of SecondStrategy, resulting in the execution of its algorithm. The same process is repeated once more with a ThirdStrategy. This code illustrates the dynamic and interchangeable nature of the pattern.

Python

from abc import ABC, abstractmethod


class Strategy(ABC):
    @abstractmethod
    def algorithm(self):
        pass


class FirstStrategy(Strategy):
    def algorithm(self):
        print('Executing first strategy')


class SecondStrategy(Strategy):
    def algorithm(self):
        print('Executing second strategy')


class ThirdStrategy(Strategy):
    def algorithm(self):
        print('Executing third strategy')

The provided Python code snippet defines a set of classes to implement the Strategy design pattern using abstract base classes. The Strategy class, derived from the ABC (Abstract Base Class) module, declares an abstract method algorithm(), enforcing a common interface for all concrete strategies. Three concrete strategy classes, namely FirstStrategy, SecondStrategy, and ThirdStrategy, inherit from the Strategy base class and implement the algorithm() method. Each concrete strategy prints a message indicating the execution of its specific algorithm.

class StrategyContext:
    def __init__(self, strategy: Strategy):
        self._strategy = strategy

    @property
    def strategy(self):
        return self._strategy

    @strategy.setter
    def strategy(self, strategy: Strategy):
        self._strategy = strategy

    def execute(self):
        self._strategy.algorithm()

The above code defines a StrategyContext class. The class has a private member _strategy representing the current strategy, initialized through the constructor. It provides a property method strategy for accessing the current strategy and a setter method allowing dynamic changes to the strategy. The execute method triggers the execution of the current strategy's algorithm. This design promotes encapsulation and flexibility, enabling the dynamic switching of strategies at runtime by assigning new strategy instances through the property setter.

context = StrategyContext(FirstStrategy())
context.execute()

context.strategy = SecondStrategy()
context.execute()

context.strategy = ThirdStrategy()
context.execute()

Initially, an instance of StrategyContext is created, initialized with a FirstStrategy. The execute method is then called, executing the algorithm associated with the first strategy. Subsequently, the strategy is dynamically changed by assigning a new instance of SecondStrategy to the context.strategy, and the execute method is invoked again, resulting in the execution of the second strategy's algorithm. This process is repeated once more with a ThirdStrategy, demonstrating the dynamic nature of the Strategy design pattern. The code showcases how different strategies can be seamlessly integrated into the context, promoting flexibility and adaptability by allowing the client to switch between algorithms at runtime.

Summary

The Strategy design pattern, a versatile behavioral design pattern, provides a dynamic and interchangeable approach to algorithm usage within a system. By establishing a common interface, often through an abstract base class or interface, the pattern accommodates multiple concrete classes (strategies) to implement specific algorithms. Encouraging encapsulation, it separates client code from the intricacies of algorithm implementation and facilitates the seamless switching of strategies at runtime. This modular methodology enhances code flexibility, maintainability, and reusability, making it applicable across various programming languages where the essence of the pattern remains consistent. The Strategy design pattern proves to be a robust solution for scenarios demanding independent adaptation of algorithms while fostering scalable and adaptable system designs.