The strategy design pattern with an example

strategy design pattern

A strategy design pattern is the most commonly used design pattern in computer programming.

In this post, we’ll start with a basic switch case statement and then we’ll modify the code into strategy design pattern.

What is a strategy design pattern?

A strategy design pattern is one of the behavioral design patterns which will route to a specific algorithm based on the input.

Here’s the definition of strategy design pattern from Wikipedia

In computer programming, the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.

here’s another from dofactory.com

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

When to use?

When we have a method that behaves differently based on the different inputs supplied then we can make use of the strategy design pattern.

In particular, if we have different business rules for different operations then we can use this strategy design pattern to have different logic for each of the operation (s).

So, if you see too many if/else blocks in a method and they vary differently based on different rules or when we have a switch case that is growing with the feature requests then we can apply the strategy design pattern.

Strategy design pattern diagram

Strategy design pattern diagram: coderethinked.com
  • Abstract the method in an interface. (Strategy interface)
  • Create separate classes for each algorithm by implementing the interface. (concrete strategy classes)
  • Have a strategy class that handles calling each of these strategy classes dynamically based on the input arguments. (Context class).

Advantages

  • The Open/Closed principle: By implementing the concrete strategies for the strategy interface, we’ll cover this principle. When creating the new strategies we will not worry about changing the existing implementations.
  • Reduced coupling: By using if/else or switch statements our code is tied to the implementation. So, changing is hard. By using this strategy, the only coupling is to the interface of the strategy. So, when changing one strategy you’d only need to change the concrete strategy you wish.
  • Testability: The if/else blocks in methods are not easily testable. The cyclomatic complexity for the if/else blocks will be increased in the future if more code flows into the method. Testing the method would require you to write many tests for a single method and should cover all the control flow branches. With the strategy design pattern, as classes would just have one single method it is easier to test.

Use case

For this article, we’ll assume that if a person walks into the room, he needs to get cool air from any source (Fan, Cooler or A.C.). It is up to the person to turn on what he needs of the day. After turning on any of those available sources we will also output what turned on.

Existing design

When the user requests for an appropriate cooling strategy we’ll just output which option user has opted for.

We can do this with a switch-case statement.

public static void PrintSelectedCoolingSystem(int collingType)
{
    switch (collingType)
    {
        case (int)CoolingSystem.Fan:
            Console.Write("Fan is turned on!");
            break;
        case (int)CoolingSystem.Cooler:
            Console.Write("Cooler is turned on!");
            break;
        case (int)CoolingSystem.AC:
            Console.Write("A.C. is turned on!");
            break;
        default:
            break;
    }
}

This is basic with hard-coded text that just prints what turned on based on passed input.

Replacing case statements logic with concrete classes

Let’s modify the above code by creating different classes for each of these cooling systems. First up, here’s the cooling system interface.

public interface ICoolingSystem
{
    void Print();
}

And here are the concrete class implementations for ICoolingSystem interface.

public class Fan : ICoolingSystem
{
    public void Print()
    {
        Console.Write("Fan is turned on!");
    }
}

public class Cooler : ICoolingSystem
{
    public void Print()
    {
        Console.Write("Cooler is turned on!");
    }
}

public class AC : ICoolingSystem
{
    public void Print()
    {
        Console.Write("A.C. is turned on!");
    }
}

With these concrete classes created let’s modify our PrintSelectedCoolingSystem method.

public static void PrintSelectedCoolingSystem(int collingType)
{
    ICoolingSystem coolingStrategy = null;

    switch (collingType)
    {
        case (int)CoolingSystem.Fan:
            coolingStrategy = new Fan();
            break;
        case (int)CoolingSystem.Cooler:
            coolingStrategy = new Cooler();
            break;
        case (int)CoolingSystem.AC:
            coolingStrategy = new AC();
            break;
    }

    coolingStrategy?.Print();

}

We’ve changed our case statements by having the instances of the cooling strategies. And finally, we’ll call the Print() method of the cooling strategy interface to print out the result for us.

With this refactoring done we are already halfway through the strategy pattern. These classes(Fan, Cooler, AC) are our concrete implementations of the Strategy interface.

The Context class

The context class in the strategy pattern is to call a particular algorithm defined by the concrete strategies. Let’s call our context class as CoolingContext.

{
    private ICoolingSystem _coolingSystem;

    /// <summary>
    /// Set appropriate cooling strategy
    /// </summary>
    /// <param name="coolingSystem"></param>
    public void SetCoolingStrategy(ICoolingSystem coolingSystem)
    {
        _coolingSystem = coolingSystem;
    }

    /// <summary>
    /// Prints the strategy set
    /// </summary>
    public void Print()
    {
        _coolingSystem?.Print();
    }
}

And here’s the PrintSelectedCoolingSystem() method again with the strategy pattern.

public static void PrintSelectedCoolingSystem(int collingType)
{
    CoolingContext collingContext = new CoolingContext();

    switch (collingType)
    {
        case (int)CoolingSystem.Fan:
            collingContext.SetCoolingStrategy(new Fan());
            break;
        case (int)CoolingSystem.Cooler:
            collingContext.SetCoolingStrategy(new Cooler());
            break;
        case (int)CoolingSystem.AC:
            collingContext.SetCoolingStrategy(new AC());
            break;
    }

    collingContext.Print();
}

We must set the strategy first by using the SetCoolingStrategy() method so that the appropriate concrete strategy is set within the context class and we can call the Print() method to print the strategy we chose.

That’s it! This is the strategy design pattern.

Strategy design pattern (complete source code):

Here’s the complete source code of strategy design pattern.

namespace StrategyDesignPattern
{
    //The strategy interface
    public interface ICoolingSystem
    {
        void Print();
    }

    //concrete implementation for Fan
    public class Fan : ICoolingSystem
    {
        public void Print()
        {
            Console.Write("Fan is turned on!");
        }
    }

    //concrete implementation for Cooler
    public class Cooler : ICoolingSystem
    {
        public void Print()
        {
            Console.Write("Cooler is turned on!");
        }
    }

    //concrete implementation for A.C.
    public class AC : ICoolingSystem
    {
        public void Print()
        {

            Console.Write("A.C. is turned on!");

        }
    }
    
    //Context class
    public class CoolingContext
    {
        private ICoolingSystem _coolingSystem;

        /// <summary>
        /// Set appropriate cooling strategy
        /// </summary>
        /// <param name="coolingSystem"></param>
        public void SetCoolingStrategy(ICoolingSystem coolingSystem)
        {
            _coolingSystem = coolingSystem;
        }

        /// <summary>
        /// Prints the strategy set
        /// </summary>
        public void Print()
        {
            _coolingSystem?.Print();
        }
    }
    
    public enum CoolingSystem
    {
         Fan = 1,
         AC,
         Cooler
    }

    //main method
    static void Main(string[] args)
    {
        Console.WriteLine("Please select a colling system:");
        int input = int.Parse(Console.ReadKey().KeyChar.ToString());
        Console.WriteLine();
        PrintSelectedCoolingSystem(input);
    }

    public static void PrintSelectedCoolingSystem(int collingType)
    {
        CoolingContext collingContext = new CoolingContext();

        switch (collingType)
        {
            case (int)CoolingSystem.Fan:
                collingContext.SetCoolingStrategy(new Fan());
                break;
            case (int)CoolingSystem.Cooler:
                collingContext.SetCoolingStrategy(new Cooler());
                break;
            case (int)CoolingSystem.AC:
                collingContext.SetCoolingStrategy(new AC());
                break;
        }

        collingContext.Print();
    }
}

Recap

In this post, we took a switch case statement and refactored into the strategy design pattern.

  • The first step is to have concrete strategy classes and move the case statement logic/implementation into these concrete strategies. Once done, plug the newly created concrete implementations in the existing code and run the app. Make sure everything works fine.
  • We’ll create a strategy interface/abstract class to abstract our method we’ll implement in concrete classes.
  • Context class calls the specific implementations at run-time. This class will have SetStrategy method that will take the Strategy type.
  • From the main method, we’ll set the strategy using context class SetStrategy() method and call the Print() method on the context object, which will then invoke concrete class print method.

References

Featured Image Credit