8.6. 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.
8.6.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
8.6.2. Problem
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')
8.6.3. Solution
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
8.6.4. Use Case - 1
Data context with cache field, with different strategies of caching: Database, Filesystem, Locmem
Gateway class with LiveHttpClient and StubHttpClient