Les principes SOLID appliqués en C# (4/5)
Publié le 30/08/2022
Par  Christophe MOMMER

Avant toute chose, il faut savoir que chacun de ces principes peut également être découvert en vidéo. La vidéo pour le second principe se trouve ici : https://youtu.be/glC7Wm9o1Ds

<< Ce truc là, il fait tout, même le café >>

Ça vous parle ce genre de remarque ? En effet, quelque chose qui fait trop de choses le fait probablement mal ou partiellement ... C'est sur la base de ce postulat que repose ce principe !

I pour Interface segregation principle

Lorsqu'on décide d'utiliser tout ce que l'objet met à notre disposition, on passe forcément par l'utilisation d'une interface. Une interface, c'est un contrat qui définit ce qu'une classe est capable de faire, ou les données qu'elle contient (définition de propriété). 

Parfois, il serait tentant d'avoir une interface très complète pour ne pas être ennuyé mais ... que se passe-t-il pour les classes qui l'implémente ou pour les classes qui les consomme ?

Ok, un exemple ?

Prenons un exemple simple, vous codez une gestion de données en utilisant le pattern repository. Vous décidez donc de faire cela proprement, en passant par une interface pour permettre plusieurs implémentations potentielle d'un repository :

public interface IRepository<T> where T : class
{
   Task Add(T value);
   Task Remove(T value);
   Task Update(T value);
   Task<IEnumerable<T>> GetAll();
   Task<T?> GetById(int id);
}

A noter que plusieurs choix arbitraires ont été fait pour ce repository, notamment que l'id est un entier, et qu'on n'autorise que des opérations unitaires. Ici, le but n'est pas de montrer le pattern repository dans les règles de l'art et de la performance, mais de montrer le principe SOLID I.

Donc, imaginons maintenant qu'une classe consomme ce repository pour afficher des personnes sur la console :

public async Task DisplayAll(IRepository<Personne> repo)
{
   foreach(var person in await repo.GetAll())
   {
       Console.WriteLine($"{person.Nom} {person.Prenom}");
   }
}

Tout se passe bien non ?

Oui, cela fonctionne bien. Cependant, la méthode a-t-elle besoin d'un repository qui autorise aussi l'écriture ? Est-ce que ce n'est pas "dangereux" d'exposer le risque que le développeur puisse aussi faire un repo.Remove(person) en plein milieu de la boucle ? (exemple poussé à l'extrême). 

En fait, c'est pour ça que le principe I est là : au lieu de faire une méga interface, faites en plusieurs petites ! Cela offre d'une part plus de flexibilité, mais d'autres part plus de sécurité. Aussi, votre code est plus honnête, car votre interface véhicule directement ce qu'elle est capable de faire !

Ah ok, et je fais comment ?

Et bien c'est simple, on fait deux interfaces au lieu d'une !

public interface IReadRepository<T> where T : class
{
   Task<IEnumerable<T>> GetAll();
   Task<T?> GetById(int id);
}

public interface IWriteRepository<T> where T : class
{
   Task Add(T value);
   Task Remove(T value);
   Task Update(T value);
}

Cela permet aux méthodes qui consomment l'interface de n'avoir que les méthodes dont elle a besoin !

Et si je veux lire ET écrire dans une seule méthode, j'ai deux paramètres pour le même objet ?

Il peut arriver qu'on ait besoin des deux, c'est OK ! Dans ce cas, il suffit de refaire l'interface principale, sans casser la flexibilité qu'on a créée :

public interface IRepository<T> : IReadRepository<T>, IWriteRepository<T> where T : class
{
}

Attention cependant à ne pas passer que par celui-ci, au risque de perdre l'avantage des multiples interfaces !