Strategy pattern is a behavioural design pattern. It allows an object to choose between different strategies in a structured way.
A strategy might be an algorithm - for example, your code might need to apply various search algorithms to a data set. You might need to be able to search a list of words for an exact match, or a matching starting letter, or for words that are anagrams.
Or a strategy might implement a policy - for example, a user login system might need to authenticate user credentials. The system might give users a choice of logging on with a name and password, or a google id, or a GitHub id, etc. Each method would have a different method of authentication.
It is quite common to encounter a situation where an object has to choose between multiple algorithms or policies.
The naive approach might be to build all the different strategies into a single class. That class might typically select the required strategy at runtime, using conditional logic (such as a set of if statements). This can lead to complex and unwieldy classes that are difficult to maintain and test.
If new strategies are required, they will need to be added to the class, which makes the problems progressively worse.
Strategy pattern implements each strategy into its own separate class so that each can be developed and tested independently. It provides a mechanism to select the correct strategy class depending on the context.
It is possible to implement the strategy pattern in such a way that new strategies can be added without making changes to the main class, leading to a loosely coupled system.
Example - searching a word list
Here is a simple class that accepts a list of words, and provides methods for searching that list of words in several ways:
class WordList: def __init__(self, words): self.words = words def find_matching(self, target): return [word for word in self.words if target==word] def find_startswith(self, target): return [word for word in self.words if word.startswith(target)]
This code is reasonably simple. The class
WordList is initialised with a value
words that must be a list of words.
find_matching method returns a list of all words that match the supplied target word. It will return an empty list if no words match.
This code uses a list comprehension to create the list of matching words.
find_startswith method returns a list of all words that start with the supplied target string. It will return an empty list if no words match.
Here we exercise the class:
wordlist = WordList(['apple', 'lemon', 'lime', 'pear']) print(wordlist.find_matching('pear')) # ['pear'] print(wordlist.find_startswith('l')) # ['lemon', 'lime']
We first create
wordlist, initialised with our fruit names.
wordlist.find_matching returns a list containing the matching string 'pear'.
wordlist.find_startswith returns a list containing the matching string 'lemon' and 'lime', the two strings that start with the letter 'l'.
The basic problem with this approach is that all the search methods are stored in the same class:
- This means that the class could get very large, and contain many diverse and complex search methods.
- Also, every time you need to add a new method, the class must be updated.
Simple strategy implementation of word searching
In the strategy pattern, we place each algorithm in a separate class. In our example, we will create the following classes:
We have creates a new interface, called
IFinder. This is the interface for all the classes that implement a finding algorithm (each different algorithm is a strategy in this pattern). The interface is very simple, it is just an
execute method that accepts a list of words and returns a list of matching words.
In the strategy pattern, the strategy interface is usually very simple, containing a single method that runs the algorithm.
Each find method is implemented as a separate class, that derives from
IFinder. Here is the code for the interface:
class IFinder: def execute(self, words): return 
execute method accepts a list of words, and return a list of matches. Because
IFinder is an interface, the
execute method just returns an empty list. The concrete finder classes have functioning
Here are two finder classes. They both implement the
class MatchingFinder(IFinder): def __init__(self, target): self.target = target def execute(self, words): return [word for word in words if self.target==word] class StartswithFinder(IFinder): def __init__(self, target): self.target = target def execute(self, words): return [word for word in words if word.startswith(self.target)]
MatchingFinder class has an
execute method that returns a list of words that exactly match the target. It works in the same way as the
find_matching method of the previous
Notice that the
target value is passed into the constructor of the
MatchingFinder class. This means that the
execute method does not need to include any parameters. This is useful, as we will see, if you later need extra finder classes that take different parameters.
StartswithFinder class implements the same algorithm as the previous
New WordList class
Here we revise the
WordList class to work with
IFinder objects. We implement a
find method that accepts an
IFinder object and calls its
execute method to find matches:
class WordList: def __init__(self, words): self.words = words def find(self, finder): return finder.execute(self.words)
Notice that the
WordList now has no knowledge of any algorithms. It delegates the matching to the
Here is the new design in action:
wordlist = WordList(['apple', 'lemon', 'lime', 'pear']) print(wordlist.find(MatchingFinder('pear'))) print(wordlist.find(StartswithFinder('l')))
We create the
WordList as before. To perform a matching search we use the following code:
This creates a
MatchingFinder with the target 'pear'.
We pass that
MatchingFinder into the
find method of
wordList. That, in turn, calls the
execute method of the
MatchingFinder to return a list of matches.
Adding a new algorithm
Suppose we wanted to add another algorithm. For example, we might want to find all words whose length is within certain bounds.
With the original code (not using strategy pattern) this would require us to extend the
WordList class with an extra method.
With strategy pattern, we don't need to change our existing code at all!
We just create a new class, and
WordList will automatically be able to use it:
class LengthFinder(IFinder): def __init__(self, minlength, maxlength): self.minlength = minlength self.maxlength = maxlength def execute(self, words): return [word for word in words if self.minlength <= len(word) <= self.maxlength] print(wordlist.find(LengthFinder(2, 4))) # ['lime', 'pear']
LengthFinder class has a constructor that accepts a min and max length, and it finds all the words with lengths that are in the range.
Notice the advantage of passing the parameters in via the constructor.
LengthFinder requires two integer values, unlike
MatchingFinder which requires a single string. But since we pass these in the constructor, both classes have the same
We have seen a simple example of how we can use the strategy design pattern in Python to implement a loosely coupled, extensible class for finding matching entries in a list of words.
The solution has several advantages:
- Each matching method is implemented in a simple, separate class.
- The overall system is structured and easy to understand.
- Extra strategies can be added without the need to modify existing code.
Join the PythonInformer Newsletter
Sign up using this form to receive an email when new content is added:
2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest