Using Python types for fun and profit: the Strategy Pattern

Introduction

The strategy pattern is a behavorial design pattern that allows you to define a family of algorithms, encapsulate them as an object, and with the help of interfaces make them interchangeable. The use of type annotations also helps to make the code more readable and maintable.

The strategy pattern looks like this:

A short explanation:

  1. The context wants to apply some sort of algorithm, and for that purpose it contains an object which implements the Strategy interface.
  2. When the Strategy is needed an object of either StrategyA or StrategyB is initiated, and the algorithm method is called.

It could be argued that this is a form of dependency injection since methods are only called on an interface and not on concrete objects.

Implementation in Python

We will start by defining a TravelStrategy interface:

class TravelStrategy:
    def travel(self, distance: int):
        pass

To keep things simple, we will define one method, travel(), which is used to simulate travelling by some sort of transport. Note that the distance has the annotation ‘int’ to make sure only whole numbers are passed to this method.

We now define our first strategy:

class TrainStrategy(TravelStrategy):
def travel(self, distance: int):
print(f"Travelling {distance} km by train")

The class TrainStrategy is a child class of Strategy and therefore has to implement the travel() method. All this method does in our simplfied example is printing out the means of transportation and the distance travelled.

The CarStrategy looks similar:

class CarStrategy(TravelStrategy):
    def travel(self, distance: int):
        print(f"Travelling {distance} km by car")

Now we come to the heart of this pattern, the TravelHub class:

class TravelHub:
    _longdistance_strategy: TravelStrategy = None
    _shortdistance_strategy: TravelStrategy = None

    def __init__(self, longdistance_strategy: TravelStrategy, shortdistance_strategy: TravelStrategy):
        self._longdistance_strategy = longdistance_strategy
        self._shortdistance_strategy = shortdistance_strategy

    def travel(self, distance: int):
        if distance > 1000:
            self._longdistance_strategy.travel(distance)
        else:
            self._shortdistance_strategy.travel(distance)

Some notes:

  1. We define two instance variables, _longdistance_strategy and _shortdistance_strategy both of type TravelStrategy. The initial value for these variables is None.
  2. To the constructor we pass a long distance strategy and a short distance strategy, that way when a travel request comes, the travel hub can either simulate the travel or return an appropiate strategy.
  3. Lastly we define the travel method to simulate some kind of travel.

Time to test

Now we can test the code:

if __name__ == "__main__":
    hub: TravelHub = TravelHub(TrainStrategy(), CarStrategy())

    hub.travel(1100)
    hub.travel(120)

Line by line:

  1. We construct a travel hub, passing the longdistance, and the short distance strategies. Note that these may be swapped or changed for something completely different.
  2. Next we simulate travel over 1100 kms and 120 kms.

Conclusion

This pattern is quite easy to implement in Python. Yet, due to its use of interface, or interface-like classes, it is quite flexible.

Notice that we actually use some kind of constructor injection, which seems well-suited for this pattern. The use of type annotations makes this option a safe choice.

Leave a Reply

Your email address will not be published. Required fields are marked *