BLOG
04 November 2021

Patterns in Action for Developers - Have You Heard of Strategy in Design Patterns?

tech

Our daily lives are full of patterns. And I do not mean the way you brew your tea in the morning or tie your shoelaces. Do not get me wrong, those are perfectly good examples of patterns, just not the ones I want to focus on. My goal is to draw your attention to patterns that help us resolve similar situations.

Example: When you are sick, you have a series of actions that you will perform, a pattern. The same is true in your daily work as a developer. If you have a problem, you probably have a pattern for it or you go to Google.

But googling is not everything. We need to be able to take the right approach to the problem. Often, someone out there has already had a similar problem. In fact, many people have probably had a similar problem and discussed possible solutions. This is how the patterns emerged. In my opinion, the strategy pattern is the most basic and the best place to start with this topic. 

What and why? 

A strategy is a pattern that guides us to define actions in a certain way. I know that the last sentence may sound a bit over the top. Let me explain. To use a strategy, we need two elements. They are called context and action. A context is usually an object that contains the data we will use in our actions. For example, personal data, order data, or other data from your current domain. The action is an operation that we need to perform based on the context data. But we do not want to hard-code it into an object. It breaks the best practices of object programming. Aside from that, it's also hard to maintain, debug, and extend. 

This is where the strategy pattern comes in. The idea is that actions, also called strategies, are being passed into context. The object knows what kind of input to pass and what kind of output to expect, but it does not know anything about the implementation details. Now if you are thinking of an ‘interface’, you are right. 

The Problem 

There should always be a problem before the solution. And the problem is this - we have travel data, which is a simple set that holds the basic information. Based on this information, we need to be able to calculate the possible additional costs that will be incurred due to visas and/or extra baggage. This is, who would be surprised, a perfect situation for a strategy pattern. 

We have context, which is in the form of travel data. We also have strategies. In this case, they are the ways to calculate the additional cost. This is a perfect example of a strategy pattern because the way to calculate the cost can vary depending on the conditions. So, the way (strategy) of calculation should be consistent with the data object. 

Enough said. Let’s dive into the code and see this pattern in action.  

The Code 

The code can be downloaded from git repo: https://github.com/Nivo1985/net_patterns. A solution contains projects that match a pattern. We start with the file TravelData.cs. In this file there is a context class. 

 public class TravelData 

    // DATA 
    public string Name { get; set; } 
    public string Departure { get; set; } 
    public string Destination { get; set; } 
    public bool ExtraLuggage { get; set; } 
    public bool InnerEuTravel { get; set; } 
     
    // Strategies 
    public IVisaCost VisaCostCalculator { get; set; } 
    public ILuggageCost LuggageCost { get; set; } 
     
    // Action 
    public int GetExtraCost() => VisaCostCalculator.GetVisaCost(this) + LuggageCost.GetLuggageCost(this); 

The class can be divided into three areas of interest. Data - this is an area where the data of the context is stored. As we can see, the data contains basic travel information. It is important to note that context says nothing about strategies. Strategies - the place where the implementation of the strategies is kept. It is important to note that only interfaces are present at this point. This way the class knows what to expect, but it does not know anything about the actual implementation. This is the advantage of this pattern. The specific implementation can and should be provided later. Based on conditions such as data or application state. With this approach, the code is open to extensions, but closed for modification. And the action - so part of the class that uses the strategies. 

"What is this strategy?" you ask. A very good question. The strategy is an interface and a set of implementations that are selected based on conditions. We will walk through this using the visa cost problem. Let us start with an interface. 

 public interface IVisaCost 

    int GetVisaCost(TravelData travelData); 

As we can see, this interface is fairly simple. It takes the travel data and returns the visa cost. So it does what it has to do. It does not bother with the details. It just tells us what the input and output should be. 

When it comes to actual implementation of the strategy, it can be found in two classes. The first class is responsible for Eu Visas and the second for those outside the Union.

 public class EuVisaCost: IVisaCost 

    public int GetVisaCost(TravelData travelData) 
    { 
        return 10; 
    } 
}

public class InternationalVisaCostIVisaCost 
{ 
    public int GetVisaCost(TravelData travelData) 
    { 
        return travelData.Destination switch 
        { 
            "USA" => 40, 
            "China" => 100, 
            _ => 50 
        }; 
    } 
}

Both classes are quite straightforward. They take the travel data and return the cost. In the case of Eu, it's super easy. For foreign countries, it's not much of a problem either. The great thing about this approach is that these strategies can easily be designed to our liking if needed. And the context class has no idea about it. 

Now that we have the strategies set up, let us see how we can bind them to an object. To do this, we are going to use a utility class. This part of the code is responsible for returning the correct strategy based on the conditions we specify. 

 public static class StrategiesGenerator 
{ 
    public static IVisaCost GetVisaCostStrategy(bool inEu) 
    { 
        return inEu switch 
        { 
            true => new EuVisaCost(), 
            _ => new InternationalVisaCost() 
        }; 
    }

public static ILuggageCost GetLuggageCostStrategy(bool extraLuggage, bool inEu) 
    { 
        return extraLuggage switch 
        { 
            false => new NoLuggageCost(), 
            _ => inEu switch 
            { 
                true => new EuLuggageCost(), 
                _ => new InternationalLuggageCost() 
            } 
        }; 
    } 
}

As we can see with the GetVisaCostStrategy method, the determining factor is that you are within the EU. If the value is true, the strategy implementation will reflect this by giving us an object of class EuVisaCost. If the value is false, we will get an object of class InternationalVisaCost that can calculate the visa cost outside the EU. 

The only thing left to do, is to construct and use the context object.

 class Program 
{ 
    static void Main(string[] args) 
    { 
        // set context data 
        var travelData = new TravelData() 
        { 
            Name "Karol", 
            Departure "Poland", 
            Destination "Spain", 
            InnerEuTravel true, 
            ExtraLuggage false 
        }; 
         
        // set strategies 
        travelData.VisaCostCalculator StrategiesGenerator.GetVisaCostStrategy(travelData.InnerEuTravel); 
        travelData.LuggageCost StrategiesGenerator.GetLuggageCostStrategy(travelData.ExtraLuggage, travelData.InnerEuTravel); 
         
        //use object with strategies 
        Console.WriteLine("First Travel Cost"); 
        Console.WriteLine(travelData.GetExtraCost().ToString()); 
         
    } 
}

The code we are most interested in is under the comment "set strategies". Since we can set the visa and baggage costs, the calculation operations are determined based on the current conditions. This is the strategy pattern in action. You are welcome. 

Summary 

The strategy pattern can be used to decouple our code and make it extensible. Now we have a clear separation between the context and the actions. I highly recommend you use it in your daily work. Let me know at karol.rogowski@softwarehut.com, if you have done it and if you get any benefits from it. 

Read Tech Stories from Experienced Devs!


Author
Karol Rogowski
Head Of Engineering

Working in IT since 2009. Currently working as Head of Engineering at SoftwareHut and as an academic teacher at Białystok Technical University. Co-founder of meet.js Białystok. Book and articles author. Father, husband, huge H.P. Lovecraft fan and terrible poker player.