A Deep Dive into the Strategy Design Pattern

The Strategy pattern is a behavioral design pattern that allows us to define a family of algorithms or behaviors, encapsulate each one as a separate class, and make them interchangeable at runtime. This pattern enables us to select the algorithm or behavior dynamically based on the specific context or requirements.

To put it simply, the strategy pattern gives us the flexibility to choose the right strategy for a task, just like selecting the most suitable tool from our toolbox. This approach makes our code more modular and adaptable, as we can plug in different strategies without making major changes to our overall program.

Problem

Imagine you're a chef and you have different ways to cook a dish. You can bake, fry, or steam the food. Each cooking method represents a strategy. Now, instead of hard-coding a specific cooking method in your recipe, you can use the strategy pattern. You define an interface called "CookingStrategy" with a common method, let's say "Cook()", and create separate classes for each cooking method that implement this interface (e.g., "BakingStrategy", "FryingStrategy", "SteamingStrategy").

At runtime, based on the recipe or customer's preference, you can dynamically select the appropriate cooking strategy. You can switch between baking, frying, or steaming without modifying the recipe code. This flexibility allows you to add new cooking strategies in the future without affecting the existing code.

Chef code example

To start, we need to create an interface called ICookingStrategy, which will act as our Strategy interface. This interface will declare a common method Cook() that represents the cooking action. It serves as a contract that all cooking strategies must adhere to.

// Define the CookingStrategy interface
public interface ICookingStrategy
{
    void Cook();
}

The next step is to create multiple classes that implement the ICookingStrategy interface. Each implementation will provide specific instructions for a particular cooking method within its Cook() method. For example, we can have a BakingStrategy class that contains instructions on how to bake the dish, a FryingStrategy class for frying, and a SteamingStrategy class for steaming.

// Implement the BakingStrategy
public class BakingStrategy : ICookingStrategy
{
    public void Cook()
    {
        Console.WriteLine("Baking the dish...");
        // Add specific instructions for baking
    }
}

// Implement the FryingStrategy
public class FryingStrategy : ICookingStrategy
{
    public void Cook()
    {
        Console.WriteLine("Frying the dish...");
        // Add specific instructions for frying
    }
}

// Implement the SteamingStrategy
public class SteamingStrategy : ICookingStrategy
{
    public void Cook()
    {
        Console.WriteLine("Steaming the dish...");
        // Add specific instructions for steaming
    }
}

Next, we'll introduce a Chef class that makes use of the strategy pattern. This class features a private member named _cookingStrategy, which is of type ICookingStrategy. This member holds the currently selected cooking strategy. With the SetCookingStrategy() method, we can specify the desired cooking strategy. To initiate the cooking process, we invoke the CookDish() method. It triggers the Cook() method of the selected strategy to cook the dish, and also handles any additional steps required for dish preparation.

// Chef class that utilizes the strategy pattern
public class Chef
{
    private ICookingStrategy _cookingStrategy;

    // Set the cooking strategy
    public void SetCookingStrategy(ICookingStrategy cookingStrategy)
    {
        _cookingStrategy = cookingStrategy;
    }

    // Cook the dish using the selected strategy
    public void CookDish()
    {
        Console.WriteLine("Preparing the dish...");
        _cookingStrategy.Cook();
        // Additional steps for dish preparation
        Console.WriteLine("Dish is ready!");
    }
}

Finally, we can utilize the pattern in the following manner

class Program
{
    static void Main(string[] args)
    {
        // Create a Chef instance
        Chef chef = new Chef();

        // Create different cooking strategies
        ICookingStrategy bakingStrategy = new BakingStrategy();
        ICookingStrategy fryingStrategy = new FryingStrategy();
        ICookingStrategy steamingStrategy = new SteamingStrategy();

        // Set the baking strategy
        chef.SetCookingStrategy(bakingStrategy);
        chef.CookDish(); // The dish will be baked

        // Set the frying strategy
        chef.SetCookingStrategy(fryingStrategy);
        chef.CookDish(); // The dish will be fried

        // Set the steaming strategy
        chef.SetCookingStrategy(steamingStrategy);
        chef.CookDish(); // The dish will be steamed
    }
}

When to use the strategy pattern

Strategy pattern is a good choice when you need flexibility and modularity in choosing and swapping out different algorithms or behaviors at runtime.

Pros and cons of using the strategy pattern

✔️ Improved code organization
✔️ Flexibility and extensibility
✔️ Code reusability
✔️ Separation of concerns
❌ Increases the complexity of the code
❌ Potential performance overhead
❌ Increased number of classes

Did you find this article valuable?

Support Theodoros Karropoulos by becoming a sponsor. Any amount is appreciated!