Back to Blog
Robotic hands holding a sword next to a workbench of tools, titled "Strategy Design Pattern"
Engineering
Dec 29, 2022
7 Min Read

Strategy Design Pattern: What, Why, and How

Introduction

Strategy is a behavioral design pattern that lets you choose between different algorithms while your program is running. This approach makes your code more flexible and reusable, and you don’t have to hard-code how algorithms are picked or write complicated if-else statements.

The pattern works by creating a common interface for a group of algorithms. Each algorithm has its own class that uses this interface. Another class, called the context, keeps a reference to a strategy object and passes work to it. This way, the context can use any strategy that follows the interface, without needing to know how it works inside. The Strategy pattern is very popular in software engineering.

What problem does it solve?

The Strategy pattern is most useful when a class needs to do the same type of task in different ways. For example, think about a video game where the player can choose between a warrior, a wizard, or a rogue. Each character attacks differently: the warrior uses a sword, the wizard casts spells, and the rogue uses daggers. How can you write the code so it picks the right attack method while the game is running, depending on the character the player chooses?

Game characters with different attack logic

Option 1: Inheritance. Create a subclass for each character type, and have each subclass override an attack() method with its own logic. This works, but it comes with real downsides.

Solving character attacks using inheritance

  1. It violates the open/closed principle. Classes should be open for extension but closed for modification. If you want to add a new character type or attack method, you have to change existing classes.
  2. It creates tight coupling between the character class and its attack method. If you want to change how a character attacks, you have to update that character’s subclass directly.
  3. It leads to duplication and low cohesion. If two character types share some behavior, you end up either copying code between subclasses or creating extra abstract classes to hold the shared code.

Option 2: Conditionals. Represent the character type with an enum or string, then branch on it inside the attack() method:

class Character:
    character_type: str

    def attack(self):
        if self.character_type == "warrior":
            print("The warrior attacks with a sword")
        elif self.character_type == "wizard":
            print("The wizard attacks with spells")
        elif self.character_type == "rogue":
            print("The rogue attacks with daggers")

This has its own drawbacks:

  1. It violates the single responsibility principle. A class should have only one reason to change, but now the Character class has two: handling character data and behavior, and picking or running the attack method.
  2. It creates high coupling and low cohesion. The character class depends on all the attack methods and needs to know how each one works.
  3. It leads to code bloat and poor readability. The attack() method gets longer and harder to read as you add more characters or conditions.

Solving it with the Strategy pattern

The fix is to separate the concern of choosing and executing an attack algorithm from the Character class itself.

First, define an interface called AttackStrategy that lists the method every attack algorithm must have. Then, make a separate class for each algorithm, like SwordAttackStrategy, SpellAttackStrategy, and DaggerAttackStrategy, each with its own way of doing things. The Character class keeps a reference to an AttackStrategy object and passes all attack logic to it, instead of handling it directly. You can use a setter method or a constructor parameter to change the strategy while the program is running.

sequenceDiagram
    participant M as MainClass
    participant Ch as Character
    participant C as ConcreteStrategy
    M->>Ch: setStrategy(strategy)
    Note over Ch,C: ConcreteStrategy is an implementation <br/> of the Strategy interface
    M->>Ch: executeStrategy()
    Ch->>C: delegate to strategy
    C->>C: perform algorithm logic
    C-->>Ch: return result
    Ch-->>M: return result

Implementation

One of the best things about the Strategy pattern is that it works in any programming language. It’s a general software engineering pattern you can use whenever you need to change an object’s behavior while the program is running. Here’s how you can use it in Python.

Class diagram for the strategy pattern

The AttackStrategy interface:

from abc import abstractmethod, ABC

class AttackStrategy(ABC):
    @abstractmethod
    def attack(self):
        pass

The concrete strategies that implement it:

class SwordAttackStrategy(AttackStrategy):
    def attack(self):
        print("The warrior attacks with a sword")

class SpellAttackStrategy(AttackStrategy):
    def attack(self):
        print("The wizard attacks with spells")

class DaggerAttackStrategy(AttackStrategy):
    def attack(self):
        print("The rogue attacks with daggers")

And the Character class, which attacks using whichever strategy it’s given:

class Character:
    def __init__(self) -> None:
        self.attack_strategy: AttackStrategy = None

    @property
    def attack_strategy(self):
        return self._attack_strategy

    @attack_strategy.setter
    def attack_strategy(self, attack_strategy: AttackStrategy):
        self._attack_strategy = attack_strategy

    def attack(self):
        if self.attack_strategy is None:
            raise ValueError("The attack strategy is not set")
        self.attack_strategy.attack()

Benefits

Here’s what you gain by moving to the Strategy pattern:

1. It adheres to the open/closed principle

Each algorithm has its own class, so if you want to add a new one, like a new character with a different attack style, you just create a new strategy class. You don’t have to change the Character class at all. This way, you add new behavior by writing new code instead of changing existing code.

2. It reduces coupling and increases cohesion

Coupling means how much different parts of your code depend on each other. Cohesion is about how closely related the code in one module is. Low coupling and high cohesion make your code easier to maintain, test, and reuse. The Strategy pattern helps with this by separating the algorithm from the class that uses it. The Character class only depends on the AttackStrategy interface and its attack() method, not on the specific implementations. Each strategy class only contains the code for its own algorithm. This leads to a clear separation of concerns.

graph TD
    A[MainClass] --> B[Create a character]
    B --> C{User set<br/>the character type}
    C -->|Warrior| D[Create and set <br/>SwordAttackStrategy object]
    C -->|Wizard| E[Create and set <br/>SpellAttackStrategy object]
    C -->|Rogue| F[Create and set <br/>DaggerAttackStrategy object]
    D --> H[Execute strategy]
    E --> H
    F --> H

3. It avoids duplication and improves readability

Since each algorithm is in its own class, you can reuse it wherever you need, instead of writing it again. Actions that every character shares, like walking or running, can stay in the Character class. Only the parts that are different between characters need to be strategies. If your game has both NPCs and player characters, they can use the same strategy classes. With no conditionals or inheritance trees in Character to choose the right algorithm, the class stays short and easy to read. You just give it a strategy object and let it handle the rest.

4. It enables choosing an algorithm at runtime

Sometimes you won’t know which algorithm you need until the program is running, or you might want the user to choose. Changing behavior is as simple as giving a different strategy object. For example, a game with easy, medium, and hard difficulty levels could use a different strategy class for each one. You don’t need to hard-code the difficulty logic or make separate versions of the game.

ProsCons
- Adheres to the open/closed principle
- Reduces coupling and increases cohesion
- Avoids code duplication and improves readability
- Enables selecting an algorithm at runtime
- Increases the number of classes and objects
- Requires a common interface for all strategies
- Can add unnecessary complexity when it isn’t needed
Filed Under

Join the Conversation

This dispatch is part of an ongoing series on the future of intelligence. Share your perspective or subscribe for more.

Weekly dispatches. No spam. Ever.