Aujourd'hui on va parler interface. Et plus particulièrement, les interfaces à tout faire. Ces interfaces qui aggrégent à elles seules tout le métier de votre entreprise parce que, je cite:
C'est quand même plus simple de les avoir au même endroit.
Alors, j'entends l'argument, mais laissez-moi vous montrer les problèmes sous-jacent.
C'est au travers d'une review de code que m'est venu l'idée de cet article. En parcourant une interface longue de 47 méthodes, préfixé de Service
, avec autant de using
que de méthodes. Cette classe m'a fait me poser une question sur la responsabilité de cette interface. En creusant davantage, je me suis rendu compte que les développeurs successifs avaient tentés de regrouper les méthodes en séparant les groupes par un saut de ligne. Comme si, inconsciemment, ils cherchaient à les grouper par logique commune, osons dire par "responsabilité" ?
Le problème ne s'arrête pas là, en parcourant cette fois les classes qui implémentent cette interface. Les développeurs ont là aussi tentés de séparer par "responsabilité" et grouper les méthodes au moyen de classes partielles ou de region
. Répartissant le code sur plusieurs fichiers, rendant la lecture du code bien plus compliquée.
But wait ! There is more ! Cette interface étant une interface fourre-tout, de nombreuses méthodes ne sont pas utiles à toutes les classes qui l'implémente. On se retrouve alors avec un certain nombre de méthodes renvoyant simplement un throw new NotImplementedException()
ajoutant énormément de pollution visuelle au code.
Quant aux tests, et bien j'aurai aimé vous donner un avis dessus. Malheureusement, dans ce cas précis il n'y en avait pas, à mon grand regret. Mais s'il y en avait, il est aisé d'imaginer qu'ils seraient tout aussi complexe à lire et à maintenir, compte tenu du nombre de méthode à tester et à couvrir pour chaque classe qui implémente l'interface.
Cette interface est donc un cas d'école assez intéressant, selon moi, des choses à ne pas faire et prouve à elle seule l'intérêt de suivre les bonnes pratiques de développement pour rendre son code plus lisible et maintenable.
Résumons donc les problèmes qui découlent de cette interface :
Intention du code : Le code est vivant, il doit nous transmettre son intention par sa simple lecture. En commencant par son nom. Lorsque l'on a du mal à définir le perimètre de notre classe, on finit bien souvent par la prefixer par
Service
ouManager
. Très vite, on se retrouve à accroitre son périmètre au fil du temps et à y mettre tout et n'importe quoi, parcequ'après tout, un manager peut tout manager, non ? Il est donc important de prendre du recul, penser son code en amont et ne pas se précipiter à la rédaction du code. Écrire du code simple est certainement la chose la plus compliquée de notre métier.Complexité accrue : Une interface contenant de nombreuses méthodes sans rapport entre elles rend la lecture et la compréhension du code plus difficile. Cela découle directement du point précédent. Si l'intention du code n'est pas clair et son perimètre trop large, il en découle naturellement de la complexité accidentelle.
Manque de cohésion : Une interface doit représenter une unité de comportement cohérente. Lorsqu'elle contient de nombreuses méthodes sans lien, elle perd sa cohésion et devient moins lisible. En cela, ce type d'interface viole le principe de résponsabilité unique préconisé par les principes SOLID.
Mauvaise abstraction : Si une interface contient trop de méthodes non liées, c'est un signe que l'interface est une mauvaise abstraction de la réalité qu'elle est censée modéliser.
Augmentation du couplage : Les interfaces avec beaucoup de méthodes peuvent augmenter le couplage dans votre système, car elles peuvent forcer les classes qui les implémentent à avoir des dépendances qu'elles n'auraient pas autrement. En cela, elle viole le principe de ségrégation des interfaces cette fois. Les clients ne doivent pas être forcés de dépendre des interfaces qu'ils n'utilisent pas.
Tests plus difficiles : Tester les classes qui implémentent de grandes interfaces est naturellement plus difficile, car vous devez écrire des tests pour chaque méthode, même si certaines d'entre elles ne sont pas pertinentes pour la classe que vous testez.
Mais bien heureusement, aucun maux n'est sans solution en matière de logiciel.
Comment ça se soigne Docteur ?
Maintenant que nous avons passé en revue les problèmes inhérent à une classe trop polyvalente, attardons nous sur les moyens de corriger et prévenir ce problème.
La solution est ici bien simple, en premier lieu, respecter les principes SOLID :
Single Responsability Principle
A class should have only one reason to change.
Interface Segregation Principle
No code should be forced to depend on methods it does not use.
Prenons l'exemple d'une interface représentant un manager permettant de gérer un utilisateur. Cette classe bien nommée, se retrouve au fil du temps à voir son périmètre s'agrandir et s'enrichir de tout ce qui a trait de près ou de loin à la gestion d'un utilisateur.
public interface IUserManager
{
void RegisterUser(string username, string password);
void DeleteUser(string username);
User GetUser(string username);
void UpdatePassword(string username, string newPassword);
void SendPasswordResetEmail(string email);
void LogIn(string username, string password);
void LogOut(string username);
void AddToRole(string username, string role);
void RemoveFromRole(string username, string role);
void SendUserNotification(string username, string message);
// ... et d'autres méthodes
}
Dans cet exemple, IUserManager
a trop de responsabilités. Elle gère l'enregistrement des utilisateurs, la suppression, la récupération, la mise à jour des mots de passe, l'envoi d'e-mails de réinitialisation de mot de passe, la gestion des sessions de connexion, la gestion des rôles et l'envoi de notifications.
Cela viole le principe de responsabilité unique car l'interface IUserManager
a plus d'une raison de changer. Ainsi que le principe de ségrégation des interfaces car une classe qui implémente IUserManager
pourrait ne pas avoir besoin de toutes ces méthodes.
Une meilleure approche serait de diviser cette interface en plusieurs interfaces plus petites, chacune ayant une seule responsabilité. Ce qui nous donnerait après refactoring, les interfaces suivantes :
public interface IUserManager
{
void RegisterUser(string username, string password);
void DeleteUser(string username);
User GetUser(string username);
void UpdatePassword(string username, string newPassword);
}
public interface ISessionManager
{
void LogIn(string username, string password);
void LogOut(string username);
}
public interface IRoleManager
{
void AddToRole(string username, string role);
void RemoveFromRole(string username, string role);
}
public interface INotificationManager
{
void SendPasswordResetEmail(string email);
void SendUserNotification(string username, string message);
}
Dans cet exemple, chaque interface a une seule responsabilité. IUserManager
gère les opérations liées à l'utilisateur, ISessionManager
gère les sessions de connexion, IRoleManager
gère les rôles des utilisateurs, et INotificationManager
gère l'envoi de notifications. Cela rend le code plus modulaire, plus facile à comprendre et à maintenir, et plus conforme aux principes SOLID.
Le nommage est aussi une des clefs majeures pour bien structurer son code. Les classes suffixées par Service
ou Manager
se retrouvent souvent à gérer plusieurs choses sans liens entre elles du fait que ces suffixes sont assez abstrait. Pensez à bien nommer vos classes, à faire ressortir l'intention par le nom. Cela vous incitera à mieux découper votre code et participera à améliorer la lisibilité.
Le mot de la fin
En conclusion, la conception d'interfaces est un aspect crucial de la programmation orientée objet. Une interface trop chargée, peut entraîner un code difficile à comprendre, à maintenir et à tester. En revanche, des interfaces bien conçues, qui sont petites, ciblées et respectent les principes SOLID, peuvent grandement améliorer la qualité du code et faciliter le développement et la maintenance de l'application. Rien ne vous empêche par la suite, par composition, d'implémenter plusieurs petites interfaces dans vos classes lorsque vous jugerez cela nécessaire.
Cela peut sembler être un effort supplémentaire au début, mais les bénéfices à long terme en termes de lisibilité, de maintenabilité et de qualité du code en valent largement la peine.
Alors, choisissez la sérénité et embrassez le pouvoir des interfaces bien conçues !