Introduction
The aim of this article is to understand the basics of the Strategy pattern and try to see when it can be useful and have a rudimentary implementation for better understanding. This is no way to implement the strategy pattern in real world applications and the example taken is also far from real. The idea of this this article is to illustrate the concept of strategy pattern only.
Background
There are many scenarios in application development where there are multiple ways of doing the same operation. We want our application to have the possibility of using all these ways of performing the operations. An example could be payment on ecommerce portals, I could choose to pay using netbanking, I could choose to use a credit card, or I can even choose PayPal for making the payment. All these are ways of performing the same operations. Though every choice will have to follow a different application logic, i.e. separate code.
One more example where we could have the same operation in multiple ways is sorting. I could sort a sequence using any one of the sorting algorithms. So when I have a situation when I want to develop the application in such a way that for any operation the user could choose one of many available options, then perhaps that is the right place to incorporate the Strategy pattern.
The philosophy of the Strategy pattern is to have multiple strategies for doing some operation and let the user choose (or some algorithm based on input data) the appropriate strategy to carry out the operation. GoF defines the Strategy pattern as “Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.”
Let us try to understand each component of this class diagram.
Strategy
: This is the interface common to all algorithms. Context uses this interface to perform the operations. ConcreteStrategy
: This is the class that implements the actual algorithm. Context
: This is the client application that performs the decision making for which strategy should be used and uses the Strategy interface (which is referring to a ConcreteStrategy object) to perform the operations.
Using the code
So to understand these concepts, let us work on a toy application for converting audio files from Wav to MP3. Here the user will have an option of selecting the source .wav file and then select the quality of the target MP3 file to be created. The user interface will provide the user with three quality values for the target file.
In our code what we will do is that we will create multiple strategies each that we will be using are to define the quality of the output. We are implementing all these separate strategies so that conversion code will remain unaffected by the code that is specific to dealing with the quality of the output. The actual code that is doing the conversion will be independent of the implementation details of these different strategies and will work in the same fashion for any selected strategy or even when the new strategies are added.
Note: This is not a real conversion application, only a dummy application but perhaps the real application can be developed on the same lines. Also, the choice of the application and design is purely to demonstrate the Strategy pattern in action (could be a bad design if we take the holistic view).
So let us first write the interface IWavToMp3ConvertionStrategy
that will be implemented by all Concrete Strategies.
interface IWavToMp3ConvertionStrategy
{
void Convert();
}
Once we have the interface ready, we can write the Concrete Strategy classes.
class LowQualityConversionStrategy : IWavToMp3ConvertionStrategy
{
public void Convert()
{
Console.WriteLine("Low quality conversion performed");
}
}
class AverageQualityConversionStrategy : IWavToMp3ConvertionStrategy
{
public void Convert()
{
Console.WriteLine("Average quality conversion performed");
}
}
class HighQualityConversionStrategy : IWavToMp3ConvertionStrategy
{
public void Convert()
{
Console.WriteLine("High quality conversion performed");
}
}
We have our concrete classes containing the logic that is different among all strategies. All the logic that is common to all the conversion feature will be present in the class that will be choosing the strategy for the actual conversion i.e. the context class in the above diagram. So let us write the Context class i.e WavToMP3Convertor
that will be using the IWavToMp3ConvertionStrategy
interface to perform the conversion.
public class WavToMP3Convertor
{
AudioFile m_fileData = null;
IWavToMp3ConvertionStrategy m_Convertor = null;
public WavToMP3Convertor(AudioFile fileData)
{
m_fileData = fileData;
}
public void Convert(IWavToMp3ConvertionStrategy convertor)
{
m_Convertor = convertor;
m_Convertor.Convert();
}
}
What is happening in this class is that the presentation layer is passing the actual file data to this class. then when the conversion will be requested a concrete strategy instance will also be passed to this class so that this class can use the passed strategy for the conversion.
The important thing to note in this code is that this code will remain unaffected even if we add more strategies. Also, if we were to create a SDK/DLL, the user will have options to create their own strategies and simply pass them to this class to use their custom strategies.
Finally we will have our presentation layer which will let the user decide the converstion strategy and pass the appropriate strategy to the conversion class.
static void Main(string[] args)
{
IWavToMp3ConvertionStrategy selectedStrategy = null;
Console.WriteLine("Assuming the file for conversion has been selected already");
AudioFile file = new AudioFile { Title = "Sample File" };
Console.WriteLine("Enter the type of output \n1. Low Quality\n2. Average Quality\n3. High Quality");
int choice = Console.Read();
if (choice == '1')
{
selectedStrategy = new LowQualityConversionStrategy();
}
else if (choice == '2')
{
selectedStrategy = new AverageQualityConversionStrategy();
}
else if (choice == '3')
{
selectedStrategy = new HighQualityConversionStrategy();
}
if (selectedStrategy != null)
{
WavToMP3Convertor convertor = new WavToMP3Convertor(file);
convertor.Convert(selectedStrategy);
}
}
Note: The selection of strategy is being done in the presentation layer in this sample application. This is rather a very contrived way of creating the concrete strategies. In real world the concrete strategies will be created either using a factory or a service locator based on the user's selection on UI.
Now when we run the application we can see that the file conversion will happen based on the user selected options and the right strategy will be used. Now let us try to compare our code with the GoF class diagram.
So we can see that IWavToMp3ConvertionStrategy
is our strategy interface, WavToMP3Convertor
is our context class and LowQualityConversionStrategy
, AverageQualityConversionStrategy
and HighQualityConversionStrategy
are our concrete strategies. The important thing to note is that the Context class i.e. WavToMP3Convertor
will remain unaffected by the concrete strategies and adding/removing strategies will not have any effect on this class.
Points of interest
In this article we have looked at the basics of strategy pattern and how we can use this pattern to decouple the client code with the actual implementation. We have used a rather contrived example just to demonstrate this pattern. The creation of concrete strategy was also not be done using a switch statement but here the intention was to show how the Context class remain unaffected by the choice of strategies and thus this simple way of creating concrete strategy. This article has been written from an absolute beginner's perspective. I hope this has been informative.
History
- January 18, 2016 - First version