In this post I explain the strategy design pattern and show an example of applying it to import data from different data sources. The strategy pattern is a behavioral design pattern that allows different implementations to be used interchangeably at runtime. An application can select the appropriate method to perform a task.
The example application lets the user import music data from a csv file source or json file source. Depending on the file type, the import strategy is chosen. The example application is part of my design patterns repository on GitHub. It requires Visual Studio 2019 and dotnet core 3.1.
Implementing the Pattern
To implement the strategy pattern we need an interface, a couple of implementations of that interface, a data object to store the imported data and a simple user interface. The object to store the imported album data is called Album
and the code is below.
public class Album
{
public string Artist { get; set; }
public string Title { get; set; }
public int NumberOfTracks { get; set; }
public int ReleaseYear { get; set; }
}
The interface contains one method to import a collection of albums.
public interface IImportStrategy
{
IEnumerable<Album> ImportAlbums(string filePath);
}
There are two implementations of the interface. One for csv formatted files and one for json formatted files. Both are below.
public class CsvStrategy : IImportStrategy
{
public IEnumerable<Album> ImportAlbums(string filePath)
{
string[] lines = File.ReadAllLines(filePath);
List<Album> result = new List<Album>();
for (int i = 1; i < lines.Length; i++)
{
string l = lines[i];
string[] parsedLine = l.Split(',');
result.Add(new Album
{
Artist = parsedLine[0],
Title = parsedLine[1],
ReleaseYear = int.Parse(parsedLine[2]),
NumberOfTracks = int.Parse(parsedLine[3])
});
}
return result;
}
}
public class JsonStrategy : IImportStrategy
{
public IEnumerable<Album> ImportAlbums(string filePath)
{
JsonSerializerOptions options = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
};
string content = File.ReadAllText(filePath);
List<Album> result = JsonSerializer.Deserialize<List<Album>>(content, options);
return result;
}
}
The code above is the basis of the pattern, an interface that defines a method and multiple implementations of that method. The calling code can decide which strategy it needs to use and isn't tied to one specific implementation. We're also going to create a separate class whose task it is to pick which strategy to use. This is a static class called ImportStrategyPicker.
It contains one method that takes the fileType and returns the correct IImportStrategy
implementation.
public static class ImportStrategyPicker
{
public static IImportStrategy Select(string fileType)
{
if (fileType == ".csv")
{
return new CsvStrategy();
}
else if (fileType == ".json")
{
return new JsonStrategy();
}
throw new ApplicationException("No strategy found for file type: " + fileType);
}
}
This can be called directly from the client code that executes the strategy. This would mean every time a strategy is required, the picker would need to be called first to find one. Sometimes finding the correct strategy can be a lot of work and it makes sense to store the strategy in an object so it can be used multiple times. This example contains the MusicManager
class for this purpose. It contains the Strategy used for import and the list of albums imported.
public class MusicManager
{
public List<Album> Albums { get; private set; }
public IImportStrategy Importer { get; set; }
public MusicManager()
{
Albums = new List<Album>();
}
}
The code to pick a strategy based on file type is below. I'm not going to show the entire main method, just the parts that selects and executes the strategy. This can be used in multiple ways in applications.
Console.Write("File path (csv or json): ");
string filepath = Console.ReadLine();
string extension = Path.GetExtension(filepath);
manager.Importer = ImportStrategyPicker.Select(extension.ToLower());
var albums = manager.Importer.ImportAlbums(filepath);
At this point, the albums are imported from the specified file using either the csv or json import strategy. The full sample application source code is available on GitHub. It also includes sample data files.
Conclusion
In conclusion, we looked at the strategy pattern in this post. The strategy pattern allows an application to pick the appropriate strategy at runtime to perform a task. The example application can import a list of albums from either a csv or json file source.
Top comments (4)
I believe what you are describing is the repository pattern. A data store abstraction.
In my understanding, a strategy pattern is supposed to offer different behaviour with a common interface. Usually the example has to do with calculating prices where one strategy may apply on weekdays and another might apply discounts for happy hour. This gives you the ability to change program behaviour on runtime.
Great observation! In the context of this post, the strategy pattern could be used to import data from a csv file or xml file. The program would figure out which file type is being imported and apply the correct strategy. I will unpublish and make those adjustments. Thank you for the feedback!
I have updated the post based on your feedback to apply the strategy pattern to importing data into an application from different sources.
Much better example now, great effort!