5.5. Strategy
Similar to State Pattern
No single states
Can have multiple states
Different behaviors are represented by strategy objects
Store images with compressor and filters
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:
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.
5.5.1. Problem
>>> DATABASE = [
... {'username': 'alice', 'password': 'secret', 'token': 'e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4'},
... {'username': 'bob', 'password': 'qwerty', 'token': 'b1b3773a05c0ed0176787a4f1574ff0075f7521e'},
... {'username': 'carol', 'password': '123456', 'token': '7c4a8d09ca3762af61e59520943dc26494f8941b'},
... {'username': 'dave', 'password': 'abc123', 'token': '6367c48dd193d56ea7b0baad25b19455e529f5ee'},
... {'username': 'eve', 'password': 'password1', 'token': 'e38ad214943daad1d64c102faec29de4afe9da3d'},
... {'username': 'mallory', 'password': 'NULL', 'token': 'eef19c54306daa69eda49c0272623bdb5e2b341f'},
... ]
>>>
>>>
>>> class User:
... def __init__(self, username):
... self.username = username
... self.auth = None
...
... def set_auth(self, method):
... if method not in {'password', 'token'}:
... raise ValueError('Invalid authentication method')
... self.auth = method
...
... def login(self, credential):
... if self.auth == 'password':
... for row in DATABASE:
... if row['username'] == self.username:
... if row['password'] == credential:
... print(f'User logged-in')
... return
... elif self.auth == 'token':
... for row in DATABASE:
... if row['username'] == self.username:
... if row['token'] == credential:
... print(f'User logged-in')
... return
... raise PermissionError(f'Invalid credentials')
Alice uses password authentication:
>>> alice = User('alice')
>>> alice.set_auth('password')
>>>
>>> alice.login('secret')
User logged-in
>>>
>>> alice.login('e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4')
Traceback (most recent call last):
PermissionError: Invalid credentials
Bob uses API key authentication:
>>> bob = User('bob')
>>> bob.set_auth('token')
>>>
>>> bob.login('qwerty')
Traceback (most recent call last):
PermissionError: Invalid credentials
>>>
>>> bob.login('b1b3773a05c0ed0176787a4f1574ff0075f7521e')
User logged-in
Users can switch strategies at runtime:
>>> carol = User('carol')
>>>
>>> carol.set_auth('password')
>>> carol.login('123456')
User logged-in
>>>
>>> carol.set_auth('token')
>>> carol.login('7c4a8d09ca3762af61e59520943dc26494f8941b')
User logged-in
5.5.2. Solution
>>> from abc import ABC, abstractmethod
>>>
>>>
>>> DATABASE = [
... {'username': 'alice', 'password': 'secret', 'token': 'e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4'},
... {'username': 'bob', 'password': 'qwerty', 'token': 'b1b3773a05c0ed0176787a4f1574ff0075f7521e'},
... {'username': 'carol', 'password': '123456', 'token': '7c4a8d09ca3762af61e59520943dc26494f8941b'},
... {'username': 'dave', 'password': 'abc123', 'token': '6367c48dd193d56ea7b0baad25b19455e529f5ee'},
... {'username': 'eve', 'password': 'password1', 'token': 'e38ad214943daad1d64c102faec29de4afe9da3d'},
... {'username': 'mallory', 'password': 'NULL', 'token': 'eef19c54306daa69eda49c0272623bdb5e2b341f'},
... ]
>>>
>>>
>>> class AuthMethod(ABC):
... @abstractmethod
... def login(self, username, credential):
... raise NotImplementedError
>>>
>>> class PasswordAuth(AuthMethod):
... def login(self, username, credential):
... for row in DATABASE:
... if row['username'] == username:
... if row['password'] == credential:
... print(f'User logged-in')
... return
... raise PermissionError(f'Invalid credentials')
>>>
>>> class TokenAuth(AuthMethod):
... def login(self, username, credential):
... for row in DATABASE:
... if row['username'] == username:
... if row['token'] == credential:
... print(f'User logged-in')
... return
... raise PermissionError(f'Invalid credentials')
>>>
>>>
>>> class User:
... def __init__(self, username):
... self.username = username
... self.auth = None
...
... def set_auth(self, method: AuthMethod):
... self.auth = method
...
... def login(self, credential):
... if not self.auth:
... raise ValueError('Authentication method not set')
... self.auth.login(self.username, credential)
Alice uses password authentication:
>>> alice = User('alice')
>>> alice.set_auth(PasswordAuth())
>>>
>>> alice.login('secret')
User logged-in
>>>
>>> alice.login('e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4')
Traceback (most recent call last):
PermissionError: Invalid credentials
Bob uses API key authentication:
>>> bob = User('bob')
>>> bob.set_auth(TokenAuth())
>>>
>>> bob.login('qwerty')
Traceback (most recent call last):
PermissionError: Invalid credentials
>>>
>>> bob.login('b1b3773a05c0ed0176787a4f1574ff0075f7521e')
User logged-in
Users can switch strategies at runtime:
>>> carol = User('carol')
>>>
>>> carol.set_auth(PasswordAuth())
>>> carol.login('123456')
User logged-in
>>>
>>> carol.set_auth(TokenAuth())
>>> carol.login('7c4a8d09ca3762af61e59520943dc26494f8941b')
User logged-in
5.5.3. Case Study
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')
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()
...
... # Compressing using JPEG
... # Applying Black and White filter
... image_storage.store('myfile.jpg', JPEGCompressor(), BlackAndWhiteFilter())
...
... # Compressing using PNG
... # Applying Black and White filter
... image_storage.store('myfile.png', PNGCompressor(), BlackAndWhiteFilter())
...
Compressing using JPEG
Applying Black and White filter
Compressing using PNG
Applying Black and White filter
Diagram:


5.5.4. Use Case - 1
Data context with cache field, with different strategies of caching: Database, Filesystem, Locmem
Gateway class with LiveHttpClient and StubHttpClient
5.5.5. Use Case - 2
>>> 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.