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:

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

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.

7.6.6. Further Reading

7.6.7. Assignments