The Strategy is a behavioural design pattern that lets us define a family of algorithms, put each of them into a separate class, and make their objects interchangeable.
The Strategy pattern defines a family of algorithms and then makes them interchangeable by encapsulating each as an object. Consequently, the actual operation of the algorithm can vary based on other inputs, such as which client is using it.
The basic idea of this pattern is that if we encapsulate behaviour as objects, we can then select which object to use and, thereby, which behaviour to implement based on external inputs or states. We further allow many different behaviours to be implemented without creating huge if
/then
or switch
chains.
You can find the example code of this post on GitHub
Conceptualizing the Problem
Let's say that we have a navigation app for travellers. The application is based on a map that helps users quickly orient themselves in any city.
One of the most requested features for the app was automatic route planning. The user should be able to enter an address, and the application would calculate the fastest route to that destination and display the instructions on the map.
The first version of the feature could build only routes over roads. People who travel by car are delighted. However, some people don't like to drive on their vacations. Or the city has an extensive network of footpaths that motor vehicles cannot reach. So with the next update, we implemented the option to use pedestrian routes. But then, many cities are too large to walk from one place to another. So then we updated the application and added another option to let people use public transport in their routes.
However, this was only the beginning. Some cities have public bicycles that you can use to travel from one bike station to another. So another feature is added. Later, the marketing team proposed a feature where the user can create an itinerary. The application would calculate the routes for sightseeing based on the opening and closing hours of the attractions, how crowded each attraction is and how much the recommended time to spend there, among other factors and provide suggested routes and timetables.
While, from a business perspective, the application was a tremendous success, the technical part evolved to being the Great Kingdom of Spagghettiland.
Any change to one of the routing algorithms, be it a simple bug fix or a slight adjustment to a street score, affected the whole class, increasing the chance of the application toppling over.
In addition, teamwork became inefficient at best. Our teammates, who had been hired after the successful release, have difficulties resolving merge conflicts. Implementing a new feature requires us to change the same huge class conflicting with the code produced by other people.
The Strategy pattern suggests that we take a class that implements multiple algorithms for a specific task and extract all of these algorithms into separate classes called strategies.
The original class, called the context, will store a reference to one of the strategies. The context will then delegate the work to a linked strategy object instead of executing it.
The context isn't responsible for selecting an appropriate algorithm for the job. Instead, the client passes the desired strategy to the context. In fact, the context isn't aware of how a strategy works. It can work with all strategies through the same generic interface, which only exposes a method for triggering the algorithm encapsulated within.
This way the context becomes independent of concrete strategies, so we can add new algorithms or modify existing ones without interfering with the code of the context or other algorithms.
In our navigation application, each routing algorithm can be extracted to its own class with a single Navigate
method. The method accepts an origin and destination and returns a collection of the route's checkpoints.
Even though given the same arguments, each routing class might build a different route, the main navigator class doesn't really care which algorithm is selected since its primary job is to render a set of checkpoints on the map. The class has a method for switching the active routing strategy, so its clients, such as the user interface, can replace the currently selected routing behaviour with another one.
Structuring the Strategy Pattern
In its basic implementation, the strategy pattern has 4 participants:
- Context: The Context maintains a reference to one of the concrete strategies and communicates with this object only via the strategy interface. The context calls the execution method on the linked strategy object each time it needs to run the algorithm. The context doesn't know what type of strategy it works with or how the algorithm is executed.
- Strategy: The Strategy interface is common to all concrete strategies. It declares a method the context uses to execute a strategy.
- Concrete Strategy: The Concrete Strategy implements a variation of an algorithm the context uses.
- Client: The Client creates a specific strategy object and passes it to the context. The context exposes a setter which lets clients replace the strategy associated with the context at runtime.
To demonstrate how the Strategy pattern works, we are going to create the payment system for an e-commerce platform. The Strategy pattern is used to implement the various payment methods in an e-commerce application. After selecting a product to purchase, a customer picks a payment method. Either credit card or Skrill.
Concrete strategies not only perform the actual payment but also alter the behaviour of the checkout step, providing appropriate fields to record payment details.
First, we are going to create the common IStrategy interface:
namespace Strategy.Strategies;
/// <summary>
/// Common interface for all strategies
/// </summary>
public interface IPaymentStrategy
{
bool Pay(int amount);
void Authenticate();
}
Now that we have our interface, we will implement the Concrete Strategies. The first concrete strategy will handle credit card payments. It will validate the fields of the credit card, such as the number and expiration. Note that these are just rudimentary checks, and not proper credit card validations.
using System.Text.RegularExpressions;
using Spectre.Console;
namespace Strategy.Strategies;
public class CreditCardPayment : IPaymentStrategy
{
private bool _isValidated;
public bool Pay(int amount)
{
if (!_isValidated)
return false;
AnsiConsole.WriteLine($"[green]Paying {amount} using Credit Card.[/]");
return true;
}
public void Authenticate()
{
var creditCardNumber = AnsiConsole.Prompt(
new TextPrompt<string>("Enter your card number: ")
.ValidationErrorMessage("[red]Please enter a valid card number[/]")
.Validate(e =>
{
return Regex.IsMatch(e, @"\b\d{4}(| |-)\d{4}\1\d{4}\1\d{4}\b",
RegexOptions.IgnoreCase) switch
{
false => ValidationResult.Error("[red]Provide a valid card number[/]"),
_ => ValidationResult.Success()
};
}));
var creditCardExpiration = AnsiConsole.Prompt(
new TextPrompt<string>("Enter your card expiration date: ")
.ValidationErrorMessage("[red]Please enter a valid date[/]")
.Validate(e =>
{
return Regex.IsMatch(e, @"\d{2}/\d{4}",
RegexOptions.IgnoreCase) switch
{
false => ValidationResult.Error("[red]Please enter a valid date[/]"),
_ => ValidationResult.Success()
};
}));
var creditCardCVV = AnsiConsole.Prompt(
new TextPrompt<string>("Enter your card verification value: ")
.ValidationErrorMessage("[red]Please enter a valid card verification value[/]")
.Validate(e =>
{
return Regex.IsMatch(e, @"\d{3}",
RegexOptions.IgnoreCase) switch
{
false => ValidationResult.Error("[red]Please enter a valid card verification value[/]"),
_ => ValidationResult.Success()
};
}));
AnsiConsole.WriteLine($"Card Number: [cyan]{creditCardNumber}[/]");
AnsiConsole.WriteLine($"Card Expiration Date: [cyan]{creditCardExpiration}[/]");
AnsiConsole.WriteLine($"Card CVV: [cyan]{creditCardCVV}[/]");
// Validate the card...
_isValidated = true;
}
}
The second concrete strategy implements a payment using Skrill, a payment provider. Once again, the checks are just rudimentary:
using System.Text.RegularExpressions;
using Spectre.Console;
namespace Strategy.Strategies;
/// <summary>
/// Concrete Strategy. Implements the Skrill payment method.
/// </summary>
public class SkrillPayment : IPaymentStrategy
{
private static readonly Dictionary<string, string> Database = new();
private string _email;
private string _password;
private bool _isSignedIn;
public SkrillPayment()
{
Database.Add("user@example.com", "123456");
_email = "";
_password = "";
}
public bool Pay(int amount)
{
if (!_isSignedIn)
return false;
AnsiConsole.WriteLine($"[green]Paying {amount} using PayPal.[/]");
return true;
}
/// <summary>
/// Authenticates the user.
/// </summary>
public void Authenticate()
{
while (!_isSignedIn)
{
_email = AnsiConsole.Prompt(
new TextPrompt<string>("Enter your email:")
.ValidationErrorMessage("[red]Please enter a valid email[/]")
.Validate(e =>
{
return Regex.IsMatch(e, @"^[^@\s]+@[^@\s]+\.(com|net|org|gov)$",
RegexOptions.IgnoreCase) switch
{
false => ValidationResult.Error("[red]Provide a valid email address[/]"),
_ => ValidationResult.Success()
};
}));
_password = AnsiConsole.Prompt(
new TextPrompt<string>("Enter your password: ")
.PromptStyle("red")
.Secret());
AnsiConsole.WriteLine(Verify()
? $"[green]Data verification is successful. Welcome {_email}[/]"
: "[red]Wrong username or password.[/]");
}
}
private bool Verify()
{
if (Database.ContainsKey(_email))
_isSignedIn = _password.Equals(Database[_email]);
return _isSignedIn;
}
}
Note that while the two strategies implement the same interface, they handle payments in a completely different ways. While the CardPaymentStrategy
validates the various fields of a credit card, the SkrillPaymentStrategy
implements a login mechanism.
To complete the demo application, we need a Context participant, which will maintain a reference to both the items we are buying and the strategy we are using to do so. This participant will be in the Order class:
using Strategy.Strategies;
namespace Strategy.Orders;
/// <summary>
/// Order class. Doesn't know the concrete payment method (strategy) the user has
/// picked. It uses a common strategy interface to delegate collecting payment data
/// to strategy object. It can be used to save the order to the database.
/// </summary>
public class Order
{
private int _totalCost = 0;
private bool _isClosed = false;
public void ProcessOrder(IPaymentStrategy strategy)
{
strategy.Authenticate();
}
public void AddCost(int cost)
{
_totalCost += cost;
}
public int TotalCost => _totalCost;
public void Close() => _isClosed = true;
public bool IsClosed() => _isClosed;
}
Finally, we can allow the user to select what items they want to purchase and what payment strategy they wish to use in our Program class:
using Spectre.Console;
using Strategy.Orders;
using Strategy.Strategies;
public class Program
{
private static Dictionary<string, int> _products;
private static Order order;
private static IPaymentStrategy strategy;
private static void Initialize()
{
_products = new Dictionary<string, int>
{ { "Motherboard", 220 }, { "CPU", 180 }, { "HDD", 60 }, { "RAM", 120 } };
order = new Order();
}
public static void Main()
{
Initialize();
while (!order.IsClosed())
{
do
{
var products = AnsiConsole.Prompt(
new MultiSelectionPrompt<string>()
.Title("What [green]products[/] do you want to add to the cart?")
.Required()
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more products)[/]")
.InstructionsText(
"[grey](Press [blue]<space>[/] to toggle a product, " +
"[green]<enter>[/] to accept)[/]")
.AddChoices(new[]
{
"Motherboard", "CPU", "HDD", "RAM",
}));
foreach (string product in products)
{
order.AddCost(_products[product]);
}
} while (!AnsiConsole.Confirm("Go to payment?"));
order.Close();
if (strategy == null)
{
var paymentMethod = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Select payment method:")
.PageSize(5)
.MoreChoicesText("[grey](Move up and down to reveal more payment methods)[/]")
.AddChoices(new[]
{
"Skrill",
"Credit Card"
})
);
strategy = paymentMethod.Equals("Skrill") ? new SkrillPayment() : new CreditCardPayment();
order.ProcessOrder(strategy);
}
}
}
}
If we run the app, we will first see the item selection screen, and the application will prompt us to add some products to our cart:
After we select some items, the app will prompt us to select a payment method:
If we choose Skrill payment, the app will require us to validate our username and password:
If we choose Card payment, the app will require us to validate our credit card info:
Pros and Cons of the Strategy Pattern
✔We can swap algorithms used inside an object at runtime. | ❌If we only have a couple of algorithms and they rarely change, there’s no real reason to overcomplicate the program with new classes and interfaces that come along with the pattern. |
✔ We can isolate the implementation details of an algorithm from the code that uses it. | ❌Clients must be aware of the differences between strategies to be able to select a proper one. |
✔ We can replace inheritance with composition. | ❌A lot of modern programming languages have functional type support that lets you implement different versions of an algorithm inside a set of anonymous functions. Then you could use these functions exactly as you’d have used the strategy objects, but without bloating your code with extra classes and interfaces. |
✔You can introduce new strategies without having to change the context, thus satisfying the Open/Closed Principle |
Relations with Other Patterns
- Bridge, State, Strategy and Adapter have very similar structures. Indeed, these patterns are based on composition, which is delegating work to other objects. However, they all solve different problems. A pattern isn’t just a recipe for structuring our code in a specific way. It can also communicate to other developers the problem the pattern solves.
- Command and Strategy may look similar because we can use both to parametrize an object with some action. However, they have very different intents.
- We can use the Command pattern to convert any operation into an object. The operation's parameters become fields of that object. The conversion lets us defer execution to the function, queue it, store the history of commands, and send commands to remote services.
- On the other hand, strategy usually describes different ways of doing the same thing, letting us swap the algorithms within a single context class.
- The Decorator lets us change the skin of an object, while the Strategy lets us change its guts.
- The state can be considered an extension of the Strategy pattern. Both patterns are based on composition: they change the behaviour of the context by delegating some work to helper objects. The strategy makes these objects wholly independent and unaware of each other. However, State doesn’t restrict dependencies between concrete states, letting them alter the state of the context at will.
Final Thoughts
In this article, we have discussed what is the Strategy pattern, when to use it and what are the pros and cons of using this design pattern. We then examined some use cases for this pattern and how the Strategy relates to other classic design patterns.
It's worth noting that the Strategy pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (0)