7.6. 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.
7.6.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
7.6.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
7.6.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:


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