I think that the strategy pattern is one of the most common and easily recognizable patterns. Any developer knows it and uses it in their work. It is elegant and beautiful in its simplicity and genius.
And for a long time, implementing it on the .NET platform was a pain for me. I had to write ugly code to implement it. And with the advent of .NET 8, this problem disappeared thanks to keyed services.
Let's imagine the problem. We have the IAnimal
interface and a number of classes that implement it. When executing the main application, it is necessary to create the necessary implementation for some reason and call a method for it.
The marker will be a certain enum
.
public enum AnimalType
{
Cat,
Dog,
Duck,
}
Let's implement the IAnimal
interface. In such a way that each implementation class already stores a marker indicating what kind of class it is.
public interface IAnimal
{
AnimalType Type { get; }
void MakeSound();
}
Each class will look similar to the Cat
class. I added a log to the constructor of each class. We need this to understand at what point an instance of the class was created.
public class Cat : IAnimal
{
private readonly ILogger<Cat> _logger;
public Cat(
ILogger<Cat> logger)
{
_logger = logger;
_logger.LogInformation("Cat was created");
}
public AnimalType Type => AnimalType.Cat;
public void MakeSound()
{
_logger.LogInformation("Meow");
}
}
All that remains is to implement the factory. In this, from the list of services, a list of all services that implement the IAnimal
interface is obtained. Next, depending on the value of the Type
field the required implementation is selected.
public class AnimalFactory
{
private readonly IEnumerable<IAnimal> _animals;
public AnimalFactory(
IEnumerable<IAnimal> animals)
{
_animals = animals;
}
public IAnimal GetAnimal(AnimalType animalType)
{
var animal = _animals.First(x =>
x.Type == animalType);
return animal;
}
}
If you run the code
IServiceCollection services = new ServiceCollection();
services.AddLogging(c =>
{
c.AddConsole();
});
services.AddAnimals();
ServiceProvider provider = services.BuildServiceProvider();
AnimalFactory factory = provider.GetRequiredService<AnimalFactory>();
IAnimal cat = factory.GetAnimal(AnimalType.Cat);
cat.MakeSound();
Then we will see the following information in the console.
info: Cat was created
info: Dog was created
info: Duck was created
info: Meow
The logs show that in order to obtain an instance of the Cat
class, instances of all implementations of the IAnimal
interface were created. Of course, such behavior cannot be called acceptable. Of course, in my example there is no need to make Transient
animal classes. After all, they do not contain any information. But I want to implement a strategy pattern when I need to get a new instance of a class from the factory every time.
So, the problem with the first example is that when trying to obtain the next necessary instance of the class, instances of all implementations of IAnimals
are created. Of course, a solution was found for this problem too. What if I use a switch in a factory? Then we guarantee that no other implementations will be instantiated to obtain the Cat
class.
public class AnimalFactory(IServiceProvider serviceProvider)
{
public IAnimal GetAnimal(AnimalType animalType)
{
IAnimal? animal = animalType switch
{
AnimalType.Cat => GetAnimal<Cat>(),
AnimalType.Dog => GetAnimal<Dog>(),
AnimalType.Duck => GetAnimal<Duck>(),
_ => throw new ArgumentOutOfRangeException(nameof(animalType), animalType, null)
};
if (animal == null)
throw new Exception($"unknown type '${animalType}'");
return animal;
}
private T? GetAnimal<T>() where T : class, IAnimal
{
return serviceProvider.GetRequiredService(typeof(T)) as T;
}
}
If you run the following code
IAnimal cat = factory.GetAnimal(AnimalType.Cat);
cat.MakeSound();
The following information will be displayed in the console:
info: Cat was created
info: Meow
Great. Only an instance of the Cat
class was created. But it’s just terrible that in order to add a new class that implements the IAnimal
interface, you have to change the switch in the factory every time.
A new mechanism implemented in .NET 8 - keyed services - helped solve this problem. There is no need to implement switch in the factory.
First, I added a static Type
field to the IAnimal
interface. Now it looks like this
public interface IAnimal
{
static AnimalType Type { get; }
void MakeSound();
}
After this, the implementation of animal classes will look like this.
public class Cat : IAnimal
{
private readonly ILogger<Cat> _logger;
public Cat(
ILogger<Cat> logger)
{
_logger = logger;
_logger.LogInformation("Cat was created");
}
public static AnimalType Type => AnimalType.Cat;
public void MakeSound()
{
_logger.LogInformation("Meow");
}
}
Next, I implemented the extension code for IServiceCollection
so that when adding a new class that implements IAnimal
, there was no need to change this code. Note that this code uses the new AddKeyedTransient
function.
public static class AnimalSoundsCollectionExtensions
public static class AnimalSoundsCollectionExtensions
{
public static IServiceCollection AddAnimals(this IServiceCollection services)
{
services.AddTransient<AnimalFactory>();
var animals = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type =>
typeof(IAnimal).IsAssignableFrom(type)
&& !type.IsInterface
&& !type.IsAbstract);
foreach (var animal in animals)
{
AnimalType type =
(AnimalType) animal!
.GetProperty("Type")!
.GetValue(null, null)!;
services.AddKeyedTransient(typeof(IAnimal), type, animal);
}
return services;
}
}
After all this, all that remains is to implement the factory, which will also not change when adding a new animal.
public class AnimalFactory(IServiceProvider serviceProvider)
{
public IAnimal GetAnimal(AnimalType animalType)
{
return serviceProvider.GetRequiredKeyedService<IAnimal>(animalType);
}
}
So. Thanks to the new keyed services functionality implemented in .NET 8. We now have the ability to implement the Strategy pattern with the ability to add new classes that implement the interface without having to change the factory and IServiceCollection
extension.
The code can be viewed at the link https://github.com/waksund/strategy-pattern