Design Patterns: Strategy Pattern

Strategy Pattern – defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

What problems does it solve?

The Strategy pattern can be applied in situations where part of your code is dependent upon some algorithm, but where the algorithm might vary depending on the context of what’s going on in the application. It helps to enforce both code reusability and maintenance by hiding the implementation details of this algorithm, and allowing the algorithm to be selected at runtime.

Suppose your application deals with tracking travel times between different locations. Users will select two destinations and then they’ll choose their method of travel. Let’s assume their options are [walk, drive, train, fly]. In this scenario, the part of your code that’s dependent on an algorithm is the service that you’re using to determine the travel time. The algorithm that varies is the different travel options [walk, drive, train, fly].

Let’s take a look at how a lot of developers would approach this problem. Here’s what their service would look like:

public class TravelTimeService
{
	public double CalculateTravelTime(TravelDetails travelDetails) {
		double travelTime = 0;

		switch(travelDetails.TravelType) {
			case TravelType.Walk:
				travelTime = CalculateWalkTime(travelDetails);
				break;
			case TravelType.Drive:
				travelTime = CalculateDriveTime(travelDetails);
				break;
			case TravelType.Train:
				travelTime = CalculateTrainTime(travelDetails);
				break;
			case TravelType.Fly:
				travelTime = CalculateFlyTime(travelDetails);
				break;
			default:
				throw new Exception();
		}

		return travelTime;
	}

	private double CalculateWalkTime(TravelDetails travelDetails) {
		return 1000;
	}

	private double CalculateDriveTime(TravelDetails travelDetails) {
		return 100;
	}

	private double CalculateTrainTime(TravelDetails travelDetails) {
		return 50;
	}

	private double CalculateFlyTime(TravelDetails travelDetails) {
		return 10;
	}
}

This implementation has obvious problems. First, this calculator violates the Open/Closed principle because it needs to be modified every time one of these algorithms changes, or when a new travel method is necessary. Second, we violate the Single Responsibility principle because this class is now responsible for calculating travel times for all travel methods. Third, we introduce risk by having all of our algorithms bundled up together because modifying one of them might break the calculations in another.

It is this type of problem that the Strategy Pattern can be used to resolve.

How it is implemented

What we’re missing in the above example is 3 things:

  1. Our algorithms are not encapsulated individually. They’re all exposed within the same TravelTimeService class.
  2. Our algorithms are not interchangeable.
  3. The TravelTimeService clearly cannot be changed independently of this algorithm.

Since we’ve said that the Strategy Pattern can be applied to the problem that we’ve presented, we need to follow the 3 steps that the Strategy Pattern is saying to use:

  1. Encapsulate our algorithms individually.
  2. Make the algorithms interchangeable.
  3. Ensure that changes to these algorithms doesn’t affect our client that uses them.

By addressing these one at a time we will arrive at the Strategy Pattern implementation.

1. Encapsulating our alogrithms

This part is easy, we will simply create a separate class for each of the algorithms:

public class WalkTimeCalculator {
	public double CalculateWalkTime(TravelDetails travelDetails) {
		return 1000;	
	}
}

public class DriveTimeCalculator {
	public double CalculateDriveTime(TravelDetails travelDetails) {
		return 100;	
	}
}

public class TrainTimeCalculator {
	public double CalculateTrainTime(TravelDetails travelDetails) {
		return 50;	
	}
}

public class FlyTimeCalculator {
	public double CalculateFlyTime(TravelDetails travelDetails) {
		return 10;	
	}
}

2. Making our algorithms interchangeable

Now that we’ve encapsulated our algorithms, we need to make them interchangeable. To do this, we’ll place them behind a common interface:

public interface ITravelTimeCalculator {
	double Calculate(TravelDetails travelDetails);
}

public class WalkTimeCalculator : ITravelTimeCalculator {
	public double Calculate(TravelDetails travelDetails) {
		return 1000;	
	}
}

public class DriveTimeCalculator : ITravelTimeCalculator {
	public double Calculate(TravelDetails travelDetails) {
		return 100;	
	}
}

public class TrainTimeCalculator : ITravelTimeCalculator {
	public double Calculate(TravelDetails travelDetails) {
		return 50;	
	}
}

public class FlyTimeCalculator : ITravelTimeCalculator {
	public double Calculate(TravelDetails travelDetails) {
		return 10;	
	}
}

3. Ensuring our client is independent of the algorithms

Finally, what we need to do is to make our TravelTimeService change independently of the algorithms. To do this, we will delegate the calculate behavior of this class to our algorithm implementation. The first step is to give the TravelTimeService an instance of one of these algorithms:

public class TravelTimeService
{
	public ITravelTimeCalculator Calculator { get; set; }
	public double CalculateTravelTime(TravelDetails travelDetails) {
		throw new NotImplementedException();
	}
}

Notice that we’re giving it a specific implementation of the algorithm, we’re just giving it the interface. This is what allows the interchangeability within our client.

Now we will delegate the calculate behavior to our algorithm:

public class TravelTimeService
{
	public ITravelTimeCalculator Calculator { get; set; }
	public double CalculateTravelTime(TravelDetails travelDetails) {
		return Calculator.Calculate(travelDetails);
	}
}

Putting it together

We have now followed the necessary steps to use the Strategy Pattern. Now, here’s how our code might look when switching between strategies:

static void Main(string[] args) {
    var travelDetails = new TravelDetails { StartLocation = "Dallas, TX", EndLocation = "Houston,TX" };
    var travelTimeService = new TravelTimeService();
    ITravelTimeCalculator strategy = new WalkTimeCalculator();

    travelTimeService.Calculator = strategy;
    Console.WriteLine($"Travel time for walking: {travelTimeService.CalculateTravelTime(travelDetails)}");

    strategy = new DriveTimeCalculator();
    travelTimeService.Calculator = strategy;
    Console.WriteLine($"Travel time for driving: {travelTimeService.CalculateTravelTime(travelDetails)}");

    strategy = new TrainTimeCalculator();
    travelTimeService.Calculator = strategy;
    Console.WriteLine($"Travel time for train: {travelTimeService.CalculateTravelTime(travelDetails)}");

    strategy = new FlyTimeCalculator();
    travelTimeService.Calculator = strategy;
    Console.WriteLine($"Travel time for flying: {travelTimeService.CalculateTravelTime(travelDetails)}");
    
    Console.ReadLine();
}

When running this we get the following:

Clearly we can see that we are now able to change the strategy at runtime, and our client code never has to change. Adding new modes of travel will simply be a matter of adding a new implementation of ITravelTimeCalculator, and then passing this algorithm into our client. So we no longer violate the Open/Closed Principle. Additionally, each of these algorithms has a single responsibility, making the code much easier to test and less error-prone. Finally, since each algorithm is isolated, they can’t accidentally mess each other up if one of them has to change.

Design principles

  1. Identify the things in your application that change frequently, and separate them from what stays the same.
  2. Program to an interface, not an implementation. This does not necessarily mean the interface type – an abtract class can also be an “interface” by the definition being used here.
  3. Favor composition over inheritance, i.e. build your objects out of other components (objects) instead of gaining behavior from base or parent classes.

Leave a comment

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