Principe de ségrégation des interfaces
C'est lors d'une mission de conseil pour Xerox en 1996 que l'ingénieur logiciel Robert C. Martin a défini le principle de ségrégation des interfaces (source en anglais) :
Aucun client ne devrait dépendre de méthodes qu'il n'utilise pas.
Ce principe est lié au principe de responsabilité unique en ce qu'il stipule que même les interfaces doivent disposer d'un seul objectif. Malheureusement, il est facile de l'oublier lors de l'ajout de fonctionnalités à une application.
L'objectif est de réduire le nombre de modifications du code nécessaires lors de l'ajout ou la mise à jour d'une fonctionnalité. Pour ce faire, vous devez découper votre application en plusieurs composants indépendants.
Violation du principe de ségrégation des interfaces
Le risque d'introduction de bugs lié à des modifications augmente avec l'extension de votre application. Par exemple, une méthode peut être ajoutée à une interface alors même qu'elle implémente une responsabilité supplémentaire. L'interface se retrouve alors polluée ce qui risque de faire multiplier les interfaces trop volumineuses contenant des méthodes qui implémentent plusieurs responsabilités.
Regardons un peu l'exemple suivant :
public interface ICalculatrice
{
int Somme(int nombre1, int nombre2);
int Soustraction(int nombre1, int nombre2);
int Multiplication(int nombre1, int nombre2);
int Division(int nombre1, int nombre2);
double Puissance(int nombre, double puissance);
double RacineCarree(double nombre);
}
public class CalculatriceBasique : ICalculatrice
{
public int Somme(int nombre1, int nombre2)
{
return nombre1 + nombre2;
}
public int Soustraction(int nombre1, int nombre2)
{
return nombre1 - nombre2;
}
public int Multiplication(int nombre1, int nombre2)
{
return nombre1 * nombre2;
}
public int Division(int nombre1, int nombre2)
{
return nombre1 / nombre2;
}
public double Puissance(int nombre, double puissance)
{
throw new NotSupportedException();
}
public double RacineCarree (double nombre)
{
throw new NotSupportedException();
}
}
public class CalculatriceAvancee : ICalculatrice
{
public int Somme(int nombre1, int nombre2)
{
return nombre1 + nombre2;
}
public int Soustraction(int nombre1, int nombre2)
{
return nombre1 - nombre2;
}
public int Multiplication(int nombre1, int nombre2)
{
return nombre1 * nombre2;
}
public int Division(int nombre1, int nombre2)
{
return nombre1 / nombre2;
}
public double Puissance(int nombre, double puissance)
{
return Math.Pow(nombre, puissance);
}
public double RacineCarree (double nombre)
{
return Math.Sqrt(nombre);
}
}
public class EtudiantMathBasique
{
private CalculatriceBasique Calculatrice;
public EtudiantMathBasique()
{
this.Calculatrice = new CalculatriceBasique();
}
public double Calculer(string operation, int operand1, int operand2)
{
switch (operation.ToLower())
{
case "addition":
return this.Calculatrice.Somme(operand1, operand2);
case "soustraction":
return this.Calculatrice.Soustraction(operand1, operand2);
case "multiplication":
return this.Calculatrice.Multiplication(operand1, operand2);
case "division":
return this.Calculatrice.Division(operand1, operand2);
default:
throw new ArgumentException();
}
}
}
public class EtudiantMathAvance
{
private CalculatriceAvancee Calculatrice;
public EtudiantMathAvance()
{
this.Calculatrice = new CalculatriceAvancee();
}
public double Calculer(string operation, int nombre)
{
if (operation.ToLower() == "racinecarree")
{
return this.Calculatrice.RacineCarree(nombre);
}
else
{
throw new ArgumentException();
}
}
public double Calculer(string operation, int operand1, int operand2)
{
switch (operation.ToLower())
{
case "addition":
return this.Calculatrice.Somme(operand1, operand2);
case "soustraction":
return this.Calculatrice.Soustraction(operand1, operand2);
case "multiplication":
return this.Calculatrice.Multiplication(operand1, operand2);
case "division":
return this.Calculatrice.Division(operand1, operand2);
case "puissance":
return this.Calculatrice.Puissance(operand1, operand2);
default:
throw new ArgumentException();
}
}
}
Dans cet exemple, deux calculs sont effectués pour les types d'étudiants EtudiantMathBasique
et EtudiantMathAvance
. La classe EtudiantMathBasique
utilise CalculatriceBasique
et la classe EtudiantMathAvance
CalculatriceAvancee
. Les deux calculatrices implémentent l'interface ICalculatrice
.
Si j'exécute ce code, tout se déroule normalement. Alors, quel est le problème ? Les deux calculatrices implémentent l'interface ICalculatrice
. Les méthodes incluses dans ICalculatrice
correspondent bien aux opérations de calcul, n'est-ce pas ?
Tout à fait. Mais regardez de plus près la classe CalculatriceBasique
. Elle est polluée, car elle doit implémenter les méthodes Puissance
et RacineCarree
. La classe CalculatriceBasique
n'a pas besoin de ces méthodes, alors pourquoi doit-elle les implémenter ? La classe EtudiantMathBasique
ne devrait pas avoir à gérer ces méthodes et elle n'a pas besoin d'une calculatrice disposant de ces fonctions.
Il ne s'agit que de deux méthodes, et la classe va générer de toute façon une erreur si elles sont appelées. Alors, quel est le problème ?
Eh bien, réfléchissez à l'évolution de l'application. Que se passera-t-il si CalculatriceBasique
ou EtudiantMathBasique
gagne des sous-classes ? Ces méthodes superflues qui polluent l'interface leur seront transmises.
Et que ferez-vous si vous souhaitez ajouter des opérations à CalculatriceAvancee
comme le calcul du cosinus, du sinus et de la tangente ? Il vous faudra mettre à jour l'interface ICalculatrice
, et donc ajouter de nouvelles méthodes superflues à CalculatriceBasique
.
Pour éliminer cette pollution, optez pour la ségrégation
Comment optimiser une interface volumineuse et comportant trop de fonctionnalités ? Je vous ai déjà donné la réponse plus haut : utiliser le principe de ségrégation des interfaces, qui est lié au principe de responsabilité unique. Divisons notre interface ICalculatrice
en créant des rôles plus spécifiques.
Nous allons corriger les définitions de l'interface sans toucher au reste de l'application.
Comment cela ?
En divisant l'interface ICalculatrice
en deux :
public interface IArithmetique
{
int Somme(int nombre1, int nombre2);
int Soustraction(int nombre1, int nombre2);
int Multiplication(int nombre1, int nombre2);
int Division(int nombre1, int nombre2);
}
public interface IExposants
{
double Puissance(int nombre, double puissance);
double RacineCarree(double nombre);
}
Ensuite, remplaçons l'ancien code de CalculatriceBasique
:
(public class CalculatriceBasique : ICalculatrice)
par :
public class CalculatriceBasique : IArithmetique
Vous avez ainsi dépollué la classe CalculatriceBasique
en lui retirant toutes les méthodes qu'elle n'utilisera jamais.
Ensuite, remplacez le code de CalculatriceAvancee
(public class CalculatriceAvancee : ICalculatrice)
par :
public class CalculatriceAvancee : IArithmetique, IExposants
Désormais, la classe CalculatriceAvancee
implémente les fonctionnalités arithmétiques et exponentielles des interfaces qu'elle utilisera.
En allégeant autant que possible vos interfaces, vous éviterez beaucoup de sources de pollution.
Testez par vous-même !
En résumé
D'après le principe de ségrégation des interfaces (ISP) "aucun client ne devrait dépendre de méthodes qu'il n'utilise pas".
L'objectif est d'éviter de polluer les interfaces et de les laisser devenir trop volumineuses.
Cette pollution se produit lorsqu'un objet implémente une interface disposant de comportements superflus.
Simplifiez les interfaces trop volumineuses en procédant à une ségrégation de leurs responsabilités.
Respectez le principe de ségrégation des interfaces améliore la flexibilité et la maintenabilité à long terme de votre application.
Passons enfin à la dernière lettre de notre acronyme, le "D", pour principe d'inversion des dépendances.