Parmi les principes fondamentaux qui guident les développeurs vers ce but, il y a le principe de substitution de Liskov (LSP). Conçu par Barbara Liskov en 1987, le LSP est le troisième pilier des cinq principes SOLID pour la conception de logiciels, et il répond à une question critique : lorsque vous créez une sous-classe qui hérite d'une classe parent, pouvez-vous remplacer en toute sécurité toute instance du parent par une instance de l'enfant sans rompre le programme ? La réponse de principe est une réponse définitive - - - mais seulement si la sous-classe respecte le contrat défini par le parent. Cet article explore en profondeur le principe de substitution de Liskov, fournissant des définitions claires, des exemples pratiques et des stratégies pour assurer que vos hiérarchies de classe restent fiables et flexibles.

Quel est le principe de la substitution de Liskov?

Le principe de substitution de Liskov stipule que les objets d'une superclasse doivent être remplaçables par des objets de ses sous-classes sans affecter la justesse du programme. Autrement dit, si une fonction ou méthode est conçue pour fonctionner avec un type de base, elle doit aussi fonctionner avec n'importe quel type dérivé sans nécessiter de modification ou produire des effets secondaires inattendus. Barbara Liskov a d'abord articulé cette idée dans son discours de 1987 à la Conférence sur les systèmes de programmation orientés objet, langues et applications (OOPSLA), et elle est depuis devenue une pierre angulaire de la conception orientée objet.

Il ne suffit pas qu'une sous-classe ait les mêmes signatures de méthode que son parent (conformité syntaxique); la sous-classe doit également honorer les intentions et les contraintes de la classe parent. Si une sous-classe modifie le comportement fondamental d'une méthode parent – par exemple, en jetant une exception que le parent ne lance jamais, en renvoyant une valeur qui viole le contrat parent, ou en exigeant des conditions plus strictes – alors la substitution n'est pas sûre, et le design viole la LSP.

Définition formelle et historique

Barbara Liskov , la définition formelle originale est la suivante:

-Si pour chaque objet de type S il y a un objet de type T de type [2 de type T tel que pour tous les programmes P définis en termes de T, le comportement de P est inchangé lorsque o[1 est substitué à o2[, alors S est un sous-type de T.

Cette définition souligne qu'un sous-type (S) doit être substituable à son supertype (T) dans tout programme (P) écrit en termes de T. Le comportement observable du programme doit être préservé. Ce concept est étroitement lié à la méthodologie de conception par contrat (DbC), lancée par Bertrand Meyer, où chaque méthode a des conditions préalables explicites (ce qui doit être vrai avant que la méthode ne tourne) et des conditions post-conditions (ce qui doit être vrai après).

Pour plus de détails, voir l'original Liskov et Wing paper qui formalisaient le concept. De plus, l'article Wikipedia sur LSP fournit un bon aperçu.

Pourquoi le LSP est-il important?

L'adhésion au LSP apporte plusieurs avantages critiques aux systèmes orientés objet :

  • Reliabilité et exactitude:[ Le code qui utilise un type de base peut faire confiance à toute sous-classe qui se comportera selon le contrat de type de base. Cela empêche les bugs subtils qui se produisent quand une sous-classe introduit un comportement inattendu.
  • Polymorphisme: Le polymorphisme est la capacité de traiter des objets de différentes classes par une interface commune. Sans LSP, le polymorphisme devient dangereux parce qu'il peut produire des résultats incorrects.
  • Maintenabilité et extensibilité:[ Lorsque des violations de LSP sont évitées, l'ajout de nouvelles sous-classes ne nécessite pas de modifier le code existant qui dépend du type de base. Ceci s'harmonise avec le principe ouvert/fermé (OCP) – les entités logicielles devraient être ouvertes pour l'extension mais fermées pour la modification.
  • Testabilité:[ Les tests unitaires écrits sur une classe de base peuvent être réutilisés pour valider les sous-classes. Si une sous-classe viole la LSP, ces tests échoueront, révélant l'incohérence tôt.

Par exemple, dans un système de traitement des paiements, si vous avez une classe de base avec une méthode , vous attendez toutes les sous-classes (par exemple , ) pour traiter les paiements sans erreurs ou effets secondaires que la classe de base ne prévoit pas. Les violations ici peuvent entraîner une perte de revenus ou des données corrompues.

Violations communes du principe de substitution de Liskov

La reconnaissance des violations des LSP est la première étape vers leur correction. Voici quelques modèles typiques qui rompent le principe :

Renforcement des conditions préalables

Si une méthode de classe de base s'attend à un paramètre entier, ajouter une condition dans la sous-classe que l'entier doit être positif (alors que la classe de base accepte tout entier) renforce la condition préalable. Les clients qui ont passé un nombre négatif à la classe de base échoueraient maintenant avec la sous-classe. Exemple:

// Base class
class UserService {
 public void assignRole(int userId) { ... }
}
// Subclass violation
class AdminService extends UserService {
 @Override
 public void assignRole(int userId) {
 if (userId <= 0) throw new IllegalArgumentException("Invalid ID");
 ...
 }
}

Faiblessement des conditions post-conditions

Si la classe de base garantit une certaine valeur de retour ou un effet secondaire, une sous-classe qui réduit cette garantie viole le LSP. Par exemple, une méthode de classe de base peut toujours renvoyer une chaîne non-nulle; une sous-classe qui retourne affaiblit dans certains cas la condition post.

Nouvelles exceptions

Les sous-classes ne devraient pas donner d'exceptions que la classe de base ne lance pas (sauf si ces exceptions sont des sous-classes d'exceptions déjà autorisées). Si les clients de la classe de base ne capturent que , et qu'une sous-classe lance un , la substitution causera des accidents inattendus.

Suppression ou dépassement de méthodes qui devraient être héritées

Si une sous-classe remplace une méthode pour ne rien faire (corps vide) ou pour lancer une , cela constitue une violation claire. La sous-classe ne se comporte pas comme la classe de base prévue.

Exemple classique : Rectangle, place et piège de la zone

L'exemple le plus cité de violation de LSP implique des formes géométriques. Beaucoup de manuels commencent par une classe qui a et méthodes, puis créent une sous-classe qui les outrepasse pour imposer la largeur == hauteur. Voici le problème:

// Base class
class Rectangle {
 protected int width, height;
 public void setWidth(int w) { width = w; }
 public void setHeight(int h) { height = h; }
 public int getArea() { return width * height; }
}

// Subclass violation
class Square extends Rectangle {
 @Override
 public void setWidth(int w) {
 width = height = w;
 }
 @Override
 public void setHeight(int h) {
 width = height = h;
 }
}

Considérez maintenant une fonction qui fonctionne avec un :

void resize(Rectangle r) {
 r.setWidth(5);
 r.setHeight(10);
 assert r.getArea() == 50; // Expect 50
}

Lorsque la méthode passe, elle établit la largeur à 5 et la hauteur à 10 – mais les carrés sont surbordés , la largeur à 10 est ainsi réglée, de sorte que le carré se termine par la largeur=10, la hauteur=10, la surface=100. L'affirmation échoue. n'est pas substituable pour parce qu'elle change la condition post-condition: après et , une surface rectangle=50, mais une surface carrée=100.

La correction:[ Évitez d'hériter de . Concevez plutôt une classe abstraite avec une méthode commune , et avez à la fois et mettre en œuvre indépendamment. Sinon, utilisez la composition: un carré peut être un rectangle avec des côtés égaux, mais exposer les setts qui rompent l'invariant est la vraie question. Le principe est souvent résumé comme ─ Ne rompez pas le contrat.

Exemple réel-monde: Intégrations de passerelle de paiement

Imaginez un système de commerce électronique avec une classe de base :

abstract class PaymentGateway {
 public abstract void charge(double amount);
 public void refund(double amount) { /* default implementation */ }
}

Un client qui traite des paiements peut appeler [ et plus tard . Une violation survient si remplace ] pour lancer une exception parce que l'API PayPal , pas seulement un montant, nécessite un ID de remboursement. Maintenant, tout code qui utilise sur une référence se brisera lorsque le type d'exécution est .

Comment corriger: Soit a) s'assurer que le contrat de la classe de base inclut la possibilité que ne soit pas supporté (par exemple, faire revenir un booléen de succès ou déclarer une exception contrôlée), ou b) redessiner la hiérarchie de sorte que toutes les passerelles ne supportent pas les remboursements. Par exemple, introduire une interface et ne laisser que les passerelles qui supportent les remboursements l'implémenter. Le client vérifie alors avant d'appeler le remboursement.

Comment suivre le principe de substitution de Liskov dans la pratique

La mise en oeuvre du PSL exige une discipline dans la conception et les essais.

  • Design by Contract:[ Précisez clairement les conditions préalables, postconditions et invariants pour les méthodes de classe de base. Documentez ce que chaque méthode attend et garantit. Ensuite, assurez-vous que chaque sous-classe est conforme. Des outils comme les Contrats en .NET ou JML pour Java peuvent aider à faire appliquer ces règles.
  • Favoris Interfaces over Abstract Classes: Les interfaces définissent un contrat sans détails d'implémentation. Elles sont naturellement alignées avec LSP parce que toute classe d'implémentation doit remplir l'interface entière.
  • Utiliser la composition sur l'héritage: Lorsqu'une sous-classe doit passer outre le comportement au point de rompre le contrat de base, elle est souvent préférable à utiliser la composition. Par exemple, au lieu d'hériter de , avoir une classe qui utilise en interne une mais n'expose pas les setters.
  • Test de substituabilité:[ Écrire des tests paramétrés qui exécutent les mêmes scénarios à la fois pour les types de base et dérivés. Si un test passe avec le type de base mais échoue avec un type dérivé, vous avez une violation LSP. Cette pratique est particulièrement puissante lorsque l'on utilise des doubles de test.
  • Check for Covariant Retour Types:[ Certaines langues permettent des types de retour covariants (par exemple, une méthode de sous-classe peut renvoyer un type plus spécifique que la base). Ceci est très bien tant qu'il ne change pas la condition post. Assurez-vous que l'objet renvoyé satisfait toujours toutes les attentes de la valeur de retour de type de base.
  • Éviter les méthodes de béton Indiscrimination : Si vous ressentez le besoin de passer outre une méthode concrète dans une sous-classe, questionnez si l'héritage est le bon outil. Peut-être que la classe de base était trop concrète.

LSP et les autres principes SOLID

Le PSL est étroitement lié aux autres principes SOLID, en particulier le principe ouvert/fermé (PCO) et le principe d'inversion de la dépendance (PID) :

  • LSP et OCP: OCP déclare que les classes devraient être ouvertes pour l'extension mais fermées pour modification. LSP s'assure que les extensions (sous-classes) ne cassent pas le code existant qui utilise le type de base. Sans LSP, l'ajout d'une nouvelle sous-classe nécessiterait la modification du code client pour gérer le nouveau comportement, violant OCP.
  • LSP et DIP:[ DIP conseille selon les abstractions, pas les concrétions. LSP est essentiel ici parce que l'abstraction (interface ou classe de base) doit être stable et fiable. Si les sous-classes violent LSP, l'abstraction n'est plus une dépendance digne de confiance, et le système devient fragile.
  • LSP et la ségrégation d'interface (ISP):[ ISP encourage les petites interfaces ciblées. Cela supporte naturellement la LSP parce qu'une petite interface définit un contrat serré qui est plus facile à honorer. Une classe qui implémente une interface surdimensionnée peut avoir du mal à remplir toutes les parties, conduisant à des violations comme lancer .

Pour un aperçu complet des principes SOLID, consultez Wikipedia , article SOLID.

Conclusion

Le principe de substitution de Liskov est bien plus qu'une gentillesse théorique; c'est un outil pratique pour construire des systèmes orientés objet qui sont sûrs de s'étendre et faciles à entretenir. En assurant que les sous-classes se comportent de manière substituable pour leurs classes mères, les développeurs peuvent compter sur le polymorphisme sans crainte de bogues cachés. Le principe encourage la réflexion attentive sur les hiérarchies de classe, conduisant à des dessins qui favorisent la composition par rapport à l'héritage, des contrats clairement définis, et des tests robustes.

N'oubliez pas que les violations de LSP apparaissent souvent comme des incohérences subtiles dans le comportement : une méthode qui lance une exception inattendue, une méthode qui ne fait rien silencieusement, ou une méthode qui modifie l'état d'une manière que la classe de base n'a jamais voulu. En étant conscient de ces drapeaux rouges, et en appliquant les lignes directrices discutées ci-dessus, vous pouvez créer des hiérarchies qui résistent au test du temps. Barbara Liskov , la perspicacité continue à guider les développeurs vers des logiciels plus fiables et flexibles.

Références externes utilisées dans cet article: