paint-brush
Implementing Strategy Pattern with .NET 8by@vdolzhenko
1,401 reads
1,401 reads

Implementing Strategy Pattern with .NET 8

by Viktoria DolzhenkoJanuary 17th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The strategy pattern is one of the most common and easily recognizable patterns. It is elegant and beautiful in its simplicity and genius. But implementing it on the.NET platform was a pain for me. And with the advent of.NET 8, this problem disappeared thanks to keyed services.
featured image - Implementing Strategy Pattern with .NET 8
Viktoria Dolzhenko HackerNoon profile picture

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,
}

Example 1

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.


Example 2

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.

Example 3

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