Strategy is one of the most well-known design patterns, and luckily, it’s also one of the easiest to understand and use. That doesn’t mean the strategy pattern isn’t valuable. Quite the contrary: this pattern is incredibly powerful in enabling you to write code that is low coupled, easy to read and maintain, adheres to the SOLID principles and the dependency injection pattern.
To help you understand the strategy pattern, this post covers the following:
To understand the examples, you’ll need some programming experience. Some level of familiarity with C# helps, but the examples will be simple enough that even readers without C# experience will be able to follow along.
If you want to run the examples as we go along, then you’ll need to have .NET installed on your machine and a code editor/IDE, such as Visual Studio Code.
Without further ado, let’s dig into the strategy design pattern.
This is how Wikipedia defines design patterns at the time of this writing:
“In software engineering, a software design pattern or design pattern is a general, reusable solution to a commonly occurring problem in many contexts in software design. A design pattern is not a rigid structure that can be transplanted directly into source code. Rather, it is a description or a template for solving a particular type of problem that can be deployed in many different situations.”
As a software engineer, it’s extremely beneficial for you to learn about design patterns since they allow you to solve problems in tried-and-tested ways. Also, design patterns serve as a form of communication. Solutions provided by design patterns are commonly known and understood by a sizeable portion of programmers, and adopting design patterns reveals the intent of code in a way that improves readability and maintainability.
The famous book, Design Patterns: Elements of Reusable Object-Oriented Software, defines the motivation for the strategy pattern as follows:
“Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.”
In plain terms, you have a task that needs to be done in your program and several ways to do it. Therefore, you would be wise to create some isolation between the code that needs to perform this task and the ways in which this task can be performed. Creating a degree of isolation increases maintainability and helps you avoid code duplication and high coupling.
The strategy pattern avoid these pitfalls by enabling you to do the following:
How does that work in practice?
Learning how the strategy pattern works starts by understanding its components.
The strategy pattern relies on three components: strategy, concrete strategy, and context.
In C#, a common way of implementing the three components would be the following:
Who decides which one of the concrete strategies should be used? Normally, this is the job of the context’s client. In other words, the code that uses the context is usually responsible for deciding, which one of the available strategies to provide to the context, based on your criteria.
In short, the interaction goes like this:
Things will become clearer after you see an example.
Let’s say you have a payroll application and would like to calculate employees’ end-of-year bonus according to their performances. After the annual review, employees receive one of the three possible classifications: meet expectations, exceed expectations, and below expectations, and receive bonuses as follows:
To implement this, first we should create the strategy. Let’s use an interface for that:
public interface IBonusCalculationStrategy
{
decimal CalculateBonus(decimal baseSalary);
}
Now we’ll create three classes, one for each of the performance classifications:
public class MeetExpectationsStrategy : IBonusCalculationStrategy
{
public decimal CalculateBonus(decimal baseSalary)
{
return baseSalary * 0.10m;
}
}
public class ExceedExpectationsStrategy : IBonusCalculationStrategy
{
public decimal CalculateBonus(decimal baseSalary)
{
return (baseSalary * 0.20m) + 1000m;
}
}
public class BelowExpectationsStrategy : IBonusCalculationStrategy
{
public decimal CalculateBonus(decimal baseSalary)
{
return baseSalary * 0.05m;
}
}
It’s now time for the context. In our code, this will be the Employee class itself, which will store the actual state needed for the calculation:
public class Employee
{
public string Name { get; set; } = "";
public decimal BaseSalary { get; set; }
private IBonusCalculationStrategy _bonusStrategy;
public Employee(string name, decimal baseSalary, IBonusCalculationStrategy strategy)
{
Name = name;
BaseSalary = baseSalary;
_bonusStrategy = strategy;
}
public decimal CalculateYearEndBonus()
{
return _bonusStrategy.CalculateBonus(BaseSalary);
}
}
Finally, we’ll use the code by instantiating the context (that is, the employee), passing it a concrete implementation and then calculating the bonus:
class Program
{
static void Main()
{
// Create an employee with initial "Meet Expectations" rating
var employee = new Employee(
"John Doe",
50000m,
new MeetExpectationsStrategy()
);
// Calculate bonus
Console.WriteLine($"Employee: {employee.Name}");
Console.WriteLine($"Base Salary: ${employee.BaseSalary}");
Console.WriteLine($"Bonus: ${employee.CalculateYearEndBonus()}");
}
}
Using the strategy design pattern brings you many benefits:
The third item from the list above shows that the strategy design pattern is a great way to make your code adhere to the OCP (open-closed principle). The OCP is one of the SOLID principles, and it states that a given module or class should be open to expansion but close to modification. In other words, you should write your application in such a way that, in order to change or add new behavior, you create new code instead of editing the existing code. The strategy pattern is one way to accomplish that goal.
Finally, even though we’ve been talking about different behaviors, strategy pattern can be used to execute multiple implementations of the same behavior. Think, for instance, of sorting algorithms. A bubble sort is implemented – and performs – very differently than a quick sort, but both have the same externally observable behavior. Regardless, the input gets sorted correctly.
Like any other design pattern, the strategy pattern isn’t a silver bullet. Strategy patterns have potential limitations and pitfalls you should be aware of before adopting.
First of all, using a higher number of classes increased complexity of your code. Most of the time, you won’t be using a gigantic number of strategies, but if that’s the case, you might want to reconsider adopting this pattern.
The code that creates the context must choose which strategy to use. This creates coupling between the client and strategy classes, as the client needs to know about all available strategies. Using the Abstract Factory pattern can help reduce this coupling.
Finally, there’s potential performance overhead due to the increased number of allocations (in a scenario in which the client creates many strategies and switches between them).
Before wrapping up, here are some best practices related to strategy. Some of those are C# specific, but most can be applied to any object-oriented language:
The strategy design pattern is quite simple when compared to other patterns, but don’t underestimate the value the pattern provides. You can really improve your code and overall application performance by using the pattern wisely.
Indiscriminate usage of strategies can significantly impact performance. That’s why using an application monitoring tool such as Stackify is key. With Stackify, you can monitor your application in real time, identify concerning trends and proactively resolve issues before they turn into a real issue. To see how Stackify can help you use strategy design pattern and optimize overall application performance, start your free Stackify trial today.
Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.