Sign Up
Hero

Strategy Design Pattern in Python

Learn strategy design pattern to write better code in Python

While you are developing software applications, you might run into some problems implementing the feature you need. Software design patterns are typical solutions for some of these commonly occurring problems while developing software applications using Object-oriented design. In this article, let's learn about one of the popular design patterns, Strategy pattern, and its implementation in Python.

Introduction

Before diving into the strategy pattern, you should be familiar with some of the basics concepts of Object-Oriented Programming (OOP). The entire concept of design patterns revolves around classes and objects. The design patterns are more high-level solutions for commonly occurring problems. They are like the blueprint to solve a specific problem. They are not confined to a single programming language. You can use design patterns in any programming language that supports object-oriented programming; the process will be the same while the syntax changes. There are several types of design patterns, including Creational, Structural, and Behavioral patterns. Creational patterns are about different ways to create objects that increase the flexibility of our code. Structural patterns are about relations between the objects, making larger structures flexible using objects and classes. Behavioral patterns are about effective communications and interactions between objects.

Strategy

Strategy Pattern is a design pattern that enables our application to select algorithms at runtime, making our application flexible. The original book on design patterns written by GoF states that "Strategy pattern intends to define a family of algorithms, encapsulates each one, and make them interchangeable." More specifically, it lets you define a set of algorithms that are interchangeable according to some factors at runtime. Strategy Pattern falls under the category of behavioral design patterns as it enables an algorithm's behavior to be selected at runtime.

Usage

While developing software applications, you may have a few alternatives to accomplish something in your code. Depending on your client choices, data sources, or other factors, you want to do something different without changing the code. You often tend to define algorithms using conditional statements for different situations in the main class of the code. But it is not an elegant way of writing better code. It makes the main class of your code quite long, and it becomes too hard to maintain the application.

In situations like these, the strategy pattern is an ideal solution. The strategy pattern suggests you define classes, called strategies, for your algorithms of different situations. The strategy is referenced inside the main class, called context, and the code works according to that situation. The context does not select an appropriate strategy for the case. Instead, the client passes the desired strategy to the context.

For example, if you have a chess application, you can select the difficulty level between easy, medium, or hard. The computer chooses an algorithm according to the level you choose. It is one of the best examples where the strategy pattern is used.

Strategy pattern follows the Open/close principle; a software application is open for extension but closed for modification. It means you can add any number of additional strategies without modifying the main class. It makes your code more flexible and easy to maintain.

UML Diagrams

The following is the UML diagram of the Strategy pattern.

  1. Context — It is the primary class of our application. It maintains a reference to one of the concrete strategies.
  2. Strategy — Strategy interface is common to all supported strategies. Context can communicate with other strategies only through the strategy interface.
  3. ConcreteStrategies — These are the classes that implement the algorithm using the Strategy interface.

Implementation

Let's see the step-by-step process of implementing a strategy pattern.

  1. You should first identify algorithms you want to execute as concrete strategies in the primary class.
  2. Define the context (primary class) and add a reference to the strategy, a method to set the strategy, and another method to execute the strategy. You may also define a default strategy to switch between strategies only if they do not like the default one.
## context - the primary class
class Context:
    strategy: Strategy  ## the strategy interface

    def setStrategy(self, strategy: Strategy = None) -> None:
        if strategy is not None:
            self.strategy = strategy
        else:
            self.strategy = Default()

    def executeStrategy(self) -> str:
        print(self.strategy.execute())

First, we define the strategy field for storing a reference to a strategy object, and two methods, setStrategy and executeStrategy. The setStrategy sets the strategy selected if a user selects an option, or else the default one.

  1. Define the Strategy Interface, which is common to all the concrete strategies. The Strategy interface has an abstract method that you can alter in concrete strategies.
from abc import ABC, abstractmethod

## Strategy interface
class Strategy(ABC):
    @abstractmethod
    def execute(self) -> str:
        pass
  1. Define the concrete strategies which should implement the Strategy interface. These concrete strategies must have a common method that overrides the execute method of the Strategy interface.
## Concrete strategies
class ConcreteStrategyA(Strategy):
    def execute(self) -> str:
        return "ConcreteStrategy A"

class ConcreteStrategyB(Strategy):
    def execute(self) -> str:
        return "ConcreteStrategy B"

class Default(Strategy):
    def execute(self) -> str:
        return "Default"
  1. Now, users can select the strategy they want at the runtime. Create an object of context and pass a concrete strategy.
## Example application
appA = Context()
appB = Context()
appC = Context()

## selecting stratigies
appA.setStrategy(ConcreteStrategyA())
appB.setStrategy(ConcreteStrategyB())
appC.setStrategy()    ## sets to default strategy

## each object below execute different strategy with same method
appA.executeStrategy()
appB.executeStrategy()
appC.executeStrategy()

The output of the above code will be as follow:

ConcreteStrategy A
ConcreteStrategy B
Default

If you want to use another strategy, replace the ConcreteStrategy instance with the strategy you want. You can add a new concrete strategy without changing anything in the context.

Example

Let's design a rock paper scissors game using strategy pattern. You can select any strategy among rock, paper, scissors, and random to play against the computer. The below example code uses the strategy pattern to implement various strategies.

## Changing the strategy among Rock, Paper, Scissors, and Random

import random
from abc import ABC, abstractmethod

## Strategy interface 
class Strategy(ABC):
    @abstractmethod
    def selection(self) -> None:
        pass

## Concrete strategies
class Rock(Strategy):
    ## actual application will have the algorithm instead this method
    def selection(self) -> str:
        return "Rock"

class Paper(Strategy):
    def selection(self) -> str:
        return "Paper"

class Scissors(Strategy):
    def selection(self) -> str:
        return "Scissors"

class Random(Strategy):
    def selection(self) -> str:
        options = ["Rock", "Paper", "Scissors"]
        return random.choice(options)

## Context class
class Game:
    strategy: Strategy

    def __init__(self, strategy: Strategy = None) -> None:
        if strategy is not None:
            self.strategy = strategy
        else:
            self.strategy = Random()

    def play(self, sec) -> None:
        s1 = self.strategy.selection()
        s2 = sec.strategy.selection()
        if s1 == s2:
            print("It's a tie")
        elif s1 == "Rock":
            if s2 == "Scissors":
                print("Player 1 wins!")
            else:
                print("Player 2 wins!")
        elif s1 == "Scissors":
            if s2 == "Paper":
                print("Player 1 wins!")
            else:
                print("Player 2 wins!")
        elif s1 == "Paper":
            if s2 == "Rock":
                print("Player 1 wins!")
            else:
                print("Player 2 wins!")

## Example application
## PLayer 1 can select his strategy
player1 = Game(Paper())

# Player 2 gets to select
player2 = Game(Rock())

# After the second player choice, we call the play method
player1.play(player2)

According to the strategies chosen by the two players, the expected output would be:

Player 1 wins!

Test all the other cases of the game using all the other strategies. To add extra fun to the game, try creating two more strategies to the above example according to the Lizard-Spock expansion.

Conclusion

In this article, you have seen where and how to use the strategy pattern in your code. You can build flexible and maintainable software applications using the strategy pattern. You can switch between algorithms at runtime according to the user's decision without changing the code. But if your code only has a couple of algorithms, there is no need to use strategy. It just makes your code look complex with numerous classes and objects. The Strategy pattern can work as an alternative for conditional statements for selecting the behavior of the application. But the potential drawback of strategy pattern is that the users must know how strategies differ from each other to select what they need. So it would be best if you use the strategy pattern only when the variation in behavior of the application is relevant to the users. So try to make your software applications flexible using the strategy pattern.