Press "Enter" to skip to content

How to implement the Strategy Design Pattern in C#

Last updated on 2022-07-12

What is the Strategy Design Pattern?

The Strategy design pattern is a behavioral pattern used to allow changing the algorithm used to perform a function. This is done by having a separate class for each method of implementing the algorithm.

For a real-world example, imagine the function of “get lunch”. This could be implemented by “cook from scratch”, “microwave frozen meal”, “order food to be delivered”, or “go to a restaurant”.

I’ll demonstrate the strategy pattern with a calculator class that calculates the average of a list of numbers. It will use either the mean or median algorithm, with each algorithm in its own class.

Non-pattern version (multiple methods)

This version shows a calculator class using separate functions for each algorithm to calculate an average.

namespace DesignPatternsCSharpNet6.Strategy.NonPatternVersion_MultipleFunctions;
public class Calculator
{
    public double CalculateAverageByMean(List<double> values)
    {
        return values.Sum() / values.Count;
    }
    public double CalculateAverageByMedian(List<double> values)
    {
        var sortedValues = values.OrderBy(x => x).ToList();
        if(sortedValues.Count % 2 == 1)
        {
            return sortedValues[(sortedValues.Count - 1) / 2];
        }
        return (sortedValues[(sortedValues.Count / 2) - 1] + 
                sortedValues[sortedValues.Count / 2]) / 2;
    }
}

Non-pattern version (single methods)

This version uses a single function with a “switch” statement to select which algorithm to use to calculate the average.

namespace DesignPatternsCSharpNet6.Strategy.NonPatternVersion_SingleFunction;
public class Calculator
{
    public enum AveragingStrategy
    {
        Mean,
        Median
    }
    public double CalculateAverage(AveragingStrategy averagingStrategy, 
        List<double> values)
    {
        switch(averagingStrategy)
        {
            case AveragingStrategy.Mean:
                return values.Sum() / values.Count;
            case AveragingStrategy.Median:
                var sortedValues = values.OrderBy(x => x).ToList();
                if(sortedValues.Count % 2 == 1)
                {
                    return sortedValues[(sortedValues.Count - 1) / 2];
                }
                return (sortedValues[(sortedValues.Count / 2) - 1] + 
                        sortedValues[sortedValues.Count / 2]) / 2;
            default:
                throw new ArgumentException("Invalid averagingStrategy value");
        }
    }
}

Strategy Design Pattern version

The version using the design pattern is a little more complex. It uses four classes/interfaces, while the non-pattern versions only use one class. It wouldn’t be worth the extra work, for code as small as this sample.

This is the interface for the strategy.

The interface states that the “concrete strategy” classes (the classes that perform the function) need to have these properties/methods/etc.

For this example, the concrete strategy classes need a “CalculateAverage” function, which accepts a list of doubles, and returns a double.

namespace DesignPatternsCSharpNet6.Strategy.PatternVersion;
public interface IAveragingStrategy
{
    double CalculateAverage(List<double> values);
}

This class implements the “mean” method to calculate that average.

namespace DesignPatternsCSharpNet6.Strategy.PatternVersion;
public class AverageByMean : IAveragingStrategy
{
    public double CalculateAverage(List<double> values)
    {
        return values.Sum() / values.Count;
    }
}

This class implements the “median” method to calculate that average.

namespace DesignPatternsCSharpNet6.Strategy.PatternVersion;
public class AverageByMedian : IAveragingStrategy
{
    public double CalculateAverage(List<double> values)
    {
        var sortedValues = values.OrderBy(x => x).ToList();
        if(sortedValues.Count % 2 == 1)
        {
            return sortedValues[(sortedValues.Count - 1) / 2];
        }
        return (sortedValues[(sortedValues.Count / 2) - 1] + 
                sortedValues[sortedValues.Count / 2]) / 2;
    }
}

Now, the Calculator class accepts an object that implements the IAveragingStrategy interface. That object is stored in the private “_averagingStrategy” variable and used later when the CalculateAverage function is called.

namespace DesignPatternsCSharpNet6.Strategy.PatternVersion;
public class Calculator
{
    private readonly IAveragingStrategy _averagingStrategy;
    public Calculator(IAveragingStrategy averagingStrategy)
    {
        _averagingStrategy = averagingStrategy;
    }
    public double CalculateAverage(List<double> values)
    {
        return _averagingStrategy.CalculateAverage(values);
    }
}

Here is how you would instantiate a Calculator object, passing in one of the average-calculating objects.

using DesignPatternsCSharpNet6.Strategy.PatternVersion;
namespace Test.DesignPatternsCSharpNet6.StrategyPattern.PatternVersion;
public class TestCalculator
{
    private readonly List<double> _values = 
        new List<double> {10, 5, 7, 15, 13, 12, 8, 7, 4, 2, 9};
    [Fact]
    public void Test_AverageByMean()
    {
        Calculator calculator = new Calculator(new AverageByMean());
        var averageByMean = calculator.CalculateAverage(_values);
        Assert.True(ResultsAreCloseEnough(8.3636363, averageByMean));
    }
    [Fact]
    public void Test_AverageByMedian()
    {
        Calculator calculator = new Calculator(new AverageByMedian());
        var averageByMedian = calculator.CalculateAverage(_values);
        Assert.True(ResultsAreCloseEnough(8, averageByMedian));
    }
    // Because we are using doubles (floating point values), the values may not exactly match.
    // If the difference between the expected result, and the calculated result is less than .000001,
    // consider the two values as "equal".
    private bool ResultsAreCloseEnough(double expectedResult, double calculatedResult)
    {
        var difference = Math.Abs(expectedResult - calculatedResult);
        return difference < .000001;
    }
}

Where I’ve found the Strategy pattern useful

Let’s say you’re writing a program that connects to FedEx’s website to schedule shipments. Later, your company wants to also use UPS and DHL for shipments. If you used the Strategy design pattern, you might have created an IScheduleInterface interface and a FedExScheduler class that implements the interface.

Now, you can create UpsScheduler and DhlScheduler classes that implement the interface and modify the program to let the user accepts the shipper and pass in the appropriate strategy object.

The strategy pattern is also used in dependency injection.

When objects are instantiated, objects like loggers and data repositories are commonly passed in using dependency injection. Because the constructor parameters use interfaces for their datatypes, you can easily switch the exact object being used for logging or connecting to the database. This makes unit testing easier, as you can pass in “mock” objects that looks like a real logger or data repository but doesn’t have any external dependencies – like a “real” object would have.

In a real-world program, you probably wouldn’t use the strategy pattern if you only had one implementation. That’s adding complexity before it’s needed. But, once you received the request to add more implementations, that might be a time to switch to this pattern.

NOTE: This is also an example of “Composition over Inheritance”.

We could have also written this using inheritance by creating an abstract base class AverageCalculator. Then, create two child classes MeanAverageCalculator and MedianAverageCalculator to implement the different algorithms. Instead, we have the one Calculator class that we “compose” by passing in the strategy we want the calculator to use.

The source code is available at: https://github.com/CodingWithScott/DesignPatternsCSharpNet6

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.