The strategy pattern is a **behavioral pattern** that enables selecting an algorithm at runtime.
This post will demonstrate a classical OOP pattern and a more pythonic/functional strategy pattern.

## What is Strategy Pattern?

The strategy pattern is a behavioral design pattern that defines a family of algorithms and lets a program select one of them at runtime. Following is a high-level overview of the patterns:

- Declare an interface which defines a group of algorithms. This family is called
*Strategy*. - Implement concrete strategies following the interface above.

## Why do we Need Strategy Pattern?

The strategy pattern is devised to handle a few problems sharing same context by differing algorithms.

Let's assume you are trying to develop a navigation program. You already implemented most features such as retrieving current location, display the map, etc. Now, you need to implement the routing algorithm for the navigation. However, user can drive, take a bus, or walk. Therefore, you have to prepare a few routing algorithms and apply one of them at runtime.

In this case, you might want to declare a strategy *"Routing"* and implement concrete strategies such as *"CarRouting", "BusRouting", "WalkRouting."* Then, your navigation application works with whichever routing algorithm you choose.

## Python Examples

In this section, I will implement a few sorting algorithms.

### Strategy Pattern using Abstract Base Class

This is the basic implementation of the strategy pattern. I declared a abstract interface `SortStrategy`

and implemented it using bubble sort and heap sort. Thanks to the strategy interface, the `main()`

function does not depend on the concrete algorithm implementation. I can easily swap the sort algorithm by simply modifying the code for instantiating the strategy. Determining the strategy at runtime is straightforward.

`from abc import ABC, abstractmethod`

class SortStrategy(ABC):

@abstractmethod

def sort_list(data: list[float]) -> list[float]:

"""Return a sorted list of the given data."""

class BubbleSort(SortStrategy):

def sort_list(data: list[float]) -> list[float]:

...

class HeapSort(SortStrategy):

def sort_list(data: list[float]) -> list[float]:

...

def main() -> None:

# processing data

data = ...

# Instantiate a strategy

sort_strategy: SortStrategy

sort_strategy = BubbleSort()

# sort_strategy = HeapSort()

sorted = sort_strategy.sort_list(data)

### Strategy Pattern using Protocol

If the strategies do not utilize any benefits from the inheritance, using `typing.Protocol`

can be a better choice. By doing so, we can implement less coupled strategies. Most codes stay the same.

`from typing import Protocol`

class SortStrategy(Protocol):

def sort_list(data: list[float]) -> list[float]:

"""Return a sorted list of the given data."""

class BubbleSort:

def sort_list(data: list[float]) -> list[float]:

...

class HeapSort:

def sort_list(data: list[float]) -> list[float]:

...

def main() -> None:

# processing data

data = ...

# Instantiate a strategy

sort_strategy: SortStrategy

sort_strategy = BubbleSort()

sorted = sort_strategy.sort_list(data)

### Functional Programming

Python provides a high level type `Callable`

. This includes not only functions but also any objects with `__call__()`

method. By declaring the strategy interface as a `Callable`

, you can utilize python's flexible type system. It is important to note that using `Protocol`

is still a valid approach if the strategy interface requires multiple functions.

`from dataclasses import dataclass`

from typing import Callable

# define a type for strategies

SortStategy = Callable[[list[float]], list[float]]

def bubble_sort(data: list[float]) -> list[float]:

"""implement bubble sort"""

@dataclass

class SpecialSortAlgo:

algo_param1: int = 10

algo_param2: float = 0.5

def __call__(data: list[float]) -> list[float]:

"""implement sort algorithm with given parameters"""

def main() -> None:

# processing data

data = ...

# Instantiate a strategy

sort_strategy: SortStrategy

# sort_strategy = bubble_sort

sort_strategy = SpecialSortAlgo(algo_param1=15, algo_param2=0.3)

sorted = sort_strategy(data)

Since any `Callable`

with appropriate types can be a strategy, I can use both function and class. Functions are preferred when the implementation does not requires hyper-parameters. On the other hand, callable classes can provide more flexible algorithms because it can store hyper-parameters as its member variable. As you can see in the code above, it is more convenient to use `dataclasses.dataclass`

for implementing the callable class.