6.4. Strategy

  • EN: Strategy

  • PL: Strategia

  • Type: object

The Strategy design pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. The Strategy pattern lets the algorithm vary independently from the clients that use it. Here's a simple example of the Strategy pattern in Python: First, we define an abstract strategy class with a method that all concrete strategies will implement:

>>> from abc import ABC, abstractmethod
>>>
>>> class Strategy(ABC):
...     @abstractmethod
...     def execute(self, data):
...         pass

Then, we define concrete strategy classes that implement the execute method:

>>> class Sorted(Strategy):
...     def execute(self, data):
...         result = sorted(data)
...         return list(result)
>>>
>>> class ReverseSorted(Strategy):
...     def execute(self, data):
...         result = sorted(data, reverse=True)
...         return list(result)

Next, we define a context class that uses a strategy:

>>> class Context:
...     strategy: Strategy
...
...     def __init__(self, strategy: Strategy):
...         self.strategy = strategy
...
...     def run(self, data):
...         return self.strategy.execute(data)

Finally, we can use the context and strategies like this:

>>> data = [1, 2, 3, 4, 5]
>>> context = Context(strategy=Sorted())
>>> result = context.run(data)
>>> print(result)
[1, 2, 3, 4, 5]
>>> context = Context(strategy=ReverseSorted())
>>> result = context.run(data)
>>> print(result)
[5, 4, 3, 2, 1]

In this example, Sorted and ReverseSorted are interchangeable strategies that the Context class can use. The Context class doesn't need to know the details of how the strategies perform their operations. It just calls the execute method on its current strategy object.

The Strategy design pattern has several advantages:

  • Flexibility: It provides a way to define a family of algorithms, encapsulate each one, and make them interchangeable. This allows the algorithm to vary independently from the clients that use it.

  • Decoupling: The Strategy pattern helps in decoupling the code and provides a separation between the strategy interfaces and their implementation details. This makes the code easier to understand and maintain.

  • Testability: Each strategy can be tested independently which leads to more effective unit testing.

  • Extensibility: New strategies can be introduced without changing the context class, making the pattern highly extensible.

  • Dynamic Strategy Selection: The Strategy pattern allows a program to dynamically choose an algorithm at runtime. This means that the program can select the most appropriate algorithm for a particular situation.

  • Code Reusability: The Strategy pattern allows you to reuse the code by extracting the varying behavior into separate strategies. This reduces code duplication and makes the code more reusable.

  • Open/Closed Principle: The Strategy pattern adheres to the Open/Closed Principle, which states that software entities should be open for extension, but closed for modification. This means that we can add new strategies without modifying the existing code.

6.4.1. Pattern

  • Similar to State Pattern

  • No single states

  • Can have multiple states

  • Different behaviors are represented by strategy objects

  • Store images with compressor and filters

../../_images/designpatterns-strategy-pattern-1.png
../../_images/designpatterns-strategy-pattern-2.png

Figure 6.5. Strategy vs State Pattern


6.4.2. Problem

../../_images/designpatterns-strategy-solution-1.png
../../_images/designpatterns-strategy-solution-2.png

from dataclasses import dataclass


@dataclass
class ImageStorage:
    compressor: str
    filter: str

    def store(self, filename) -> None:
        if self.compressor == 'jpeg':
            print('Compressing using JPEG')
        elif self.compressor == 'png':
            print('Compressing using PNG')

        if self.filter == 'black&white':
            print('Applying Black&White filter')
        elif self.filter == 'high-contrast':
            print('Applying high contrast filter')

6.4.3. Solution

design-patterns/behavioral/img/designpatterns-strategy-solution.png

from abc import ABC, abstractmethod


class Compressor(ABC):
    @abstractmethod
    def compress(self, filename: str) -> None:
        pass

class JPEGCompressor(Compressor):
    def compress(self, filename: str) -> None:
        print('Compressing using JPEG')

class PNGCompressor(Compressor):
    def compress(self, filename: str) -> None:
        print('Compressing using PNG')


class Filter(ABC):
    @abstractmethod
    def apply(self, filename) -> None:
        pass

class BlackAndWhiteFilter(Filter):
    def apply(self, filename) -> None:
        print('Applying Black and White filter')

class HighContrastFilter(Filter):
    def apply(self, filename) -> None:
        print('Applying high contrast filter')


class ImageStorage:
    def store(self, filename: str, compressor: Compressor, filter: Filter) -> None:
        compressor.compress(filename)
        filter.apply(filename)


if __name__ == '__main__':
    image_storage = ImageStorage()

    image_storage.store('myfile.jpg', JPEGCompressor(), BlackAndWhiteFilter())
    # Compressing using JPEG
    # Applying Black and White filter

    image_storage.store('myfile.png', PNGCompressor(), BlackAndWhiteFilter())
    # Compressing using PNG
    # Applying Black and White filter

6.4.4. Assignments