Qu'est-ce qui peut mal tourner si le principe de substitution de Liskov est violé?

27

Je suivais cette question très votée sur une possible violation du principe de substitution de Liskov. Je sais ce qu'est le principe de substitution de Liskov, mais ce qui n'est pas encore clair dans mon esprit, c'est ce qui pourrait mal tourner si, en tant que développeur, je ne pense pas au principe en écrivant du code orienté objet.

Geek
la source
6
Qu'est-ce qui peut mal tourner si vous ne suivez pas LSP? Pire scénario: vous finissez par invoquer Code-thulhu! ;)
FrustratedWithFormsDesigner
1
En tant qu'auteur de cette question originale, je dois ajouter que c'était une question assez académique. Bien que les violations puissent provoquer des erreurs dans le code, je n'ai jamais eu de bogue grave ou de problème de maintenance que je puisse attribuer à une violation de LSP.
Paul T Davies
2
@Paul Donc, vous n'avez jamais eu de problème avec vos programmes en raison de hiérarchies OO compliquées (que vous n'avez pas conçues vous-même, mais que vous avez peut-être dû étendre) où les contrats ont été rompus à gauche et à droite par des gens qui n'étaient pas sûrs de l'objectif de la classe de base pour commencer? Je vous envie! :)
Andres F.
@PaulTDavies la gravité de la conséquence dépend du fait que les utilisateurs (les programmeurs qui utilisent la bibliothèque) ont une connaissance détaillée de l'implémentation de la bibliothèque (c'est-à-dire qu'ils ont accès et connaissent le code de la bibliothèque.) Finalement, les utilisateurs mettront des dizaines de vérifications conditionnelles ou construiront des wrappers autour de la bibliothèque pour tenir compte des non-LSP (comportement spécifique à la classe). Le pire des cas se produirait si la bibliothèque est un produit commercial à source fermée.
rwong
@Andres et rwong, veuillez illustrer ces problèmes avec une réponse. La réponse acceptée soutient à peu près Paul Davies en ce que les conséquences semblent mineures (une exception) qui seront rapidement remarquées et rectifiées si vous avez un bon compilateur, un analyseur statique ou un test unitaire minimal.
user949300

Réponses:

31

Je pense que c'est très bien indiqué dans cette question, qui est l'une des raisons qui ont été si bien votées.

Maintenant, lorsque vous appelez Close () sur une tâche, il y a une chance que l'appel échoue s'il s'agit d'une ProjectTask avec le statut démarré, alors que ce ne serait pas le cas s'il s'agissait d'une tâche de base.

Imaginez si vous voulez:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

Dans cette méthode, parfois l'appel .Close () explose, donc maintenant, basé sur l'implémentation concrète d'un type dérivé, vous devez changer le comportement de cette méthode par rapport à la façon dont cette méthode serait écrite si Task n'avait pas de sous-types qui pourraient être remis à cette méthode.

En raison des violations de substitution de liskov, le code qui utilise votre type devra avoir une connaissance explicite du fonctionnement interne des types dérivés pour les traiter différemment. Cela couple étroitement le code et rend généralement l'implémentation plus difficile à utiliser de manière cohérente.

Jimmy Hoffa
la source
Cela signifie-t-il qu'une classe enfant ne peut pas avoir ses propres méthodes publiques qui ne sont pas déclarées dans la classe parent?
Songo
@Songo: Pas nécessairement: c'est possible, mais ces méthodes sont "inaccessibles" à partir d'un pointeur de base (ou d'une référence ou d'une variable ou quel que soit le langage que vous utilisez) et vous avez besoin d'informations sur le type d'exécution pour interroger le type de l'objet. avant de pouvoir appeler ces fonctions. Mais c'est une question qui est fortement liée à la syntaxe et à la sémantique des langages.
Emilio Garavaglia
2
Non. C'est pour quand une classe enfant est référencée comme s'il s'agissait d'un type de la classe parent, auquel cas les membres qui ne sont pas déclarés dans la classe parent sont inaccessibles.
Chewy Gumball
1
@Phil Yep; c'est la définition du couplage étroit: changer une chose entraîne des changements dans d'autres choses. Une classe faiblement couplée peut voir son implémentation modifiée sans vous obliger à changer le code en dehors d'elle. C'est pourquoi les contrats sont bons, ils vous guident dans la façon de ne pas exiger de modifications aux consommateurs de votre objet: respectez le contrat et les consommateurs n'auront besoin d'aucune modification, ainsi un couplage lâche est réalisé. Lorsque vos consommateurs ont besoin de coder votre implémentation plutôt que votre contrat, c'est un couplage serré, et nécessaire lors de la violation de LSP.
Jimmy Hoffa
1
@ user949300 Le succès de n'importe quel logiciel pour accomplir son travail n'est pas une mesure de sa qualité, des coûts à long terme ou à court terme. Les principes de conception sont des tentatives visant à établir des directives pour réduire les coûts à long terme des logiciels, et non à faire fonctionner les logiciels. Les gens peuvent suivre tous les principes qu'ils souhaitent tout en échouant à mettre en œuvre une solution de travail, ou n'en suivre aucun et mettre en œuvre une solution de travail. Bien que les collections Java puissent fonctionner pour de nombreuses personnes, cela ne signifie pas que le coût de travailler avec elles à long terme est aussi bon marché qu'il pourrait l'être.
Jimmy Hoffa
13

Si vous ne remplissez pas le contrat qui a été défini dans la classe de base, les choses peuvent échouer en silence lorsque vous obtenez des résultats erronés.

LSP dans les états de wikipedia

  • Les conditions préalables ne peuvent pas être renforcées dans un sous-type.
  • Les post-conditions ne peuvent pas être affaiblies dans un sous-type.
  • Les invariants du supertype doivent être conservés dans un sous-type.

Si l'un de ces éléments ne tient pas, l'appelant peut obtenir un résultat auquel il ne s'attend pas.

Zavior
la source
1
Pouvez-vous penser à des exemples concrets pour le démontrer?
Mark Booth
1
@MarkBooth Le problème cercle-ellipse / carré-rectangle pourrait être utile pour le démontrer; l'article wikipedia est un bon point de départ: en.wikipedia.org/wiki/Circle-ellipse_problem
Ed Hastings
7

Prenons un cas classique des annales des questions d'entrevue: vous avez dérivé Circle d'Ellipse. Pourquoi? Parce qu'une ellipse de cercle IS-AN, bien sûr!

Sauf que ... l'ellipse a deux fonctions:

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

De toute évidence, ceux-ci doivent être redéfinis pour Circle, car un cercle a un rayon uniforme. Vous avez deux possibilités:

  1. Après avoir appelé set_alpha_radius ou set_beta_radius, les deux sont définis sur le même montant.
  2. Après avoir appelé set_alpha_radius ou set_beta_radius, l'objet n'est plus un cercle.

La plupart des langues OO ne prennent pas en charge la seconde, et pour une bonne raison: il serait surprenant de constater que votre cercle n'est plus un cercle. La première option est donc la meilleure. Mais considérez la fonction suivante:

some_function(Ellipse byref e)

Imaginez que some_function appelle e.set_alpha_radius. Mais comme e était vraiment un cercle, son rayon bêta est également étonnamment défini.

Et c'est là que réside le principe de substitution: une sous-classe doit être substituable à une superclasse. Sinon, des choses surprenantes se produisent.

Kaz Dragon
la source
1
Je pense que vous pouvez rencontrer des problèmes si vous utilisez des objets mutables. Un cercle est également une ellipse. Mais si vous remplacez une ellipse qui est également un cercle par une autre ellipse (ce que vous faites en utilisant une méthode de définition), rien ne garantit que la nouvelle ellipse sera également un cercle (les cercles sont un sous-ensemble approprié d'ellipses).
Giorgio
2
Dans un monde purement fonctionnel (avec des objets immuables), la méthode set_alpha_radius (d) aurait le type de retour ellipse (à la fois dans l'ellipse et dans la classe de cercle).
Giorgio
@Giorgio Oui, j'aurais dû mentionner que ce problème ne se produit qu'avec des objets mutables.
Kaz Dragon
@KazDragon: Pourquoi quelqu'un remplacerait-il une ellipse par un objet cercle alors que nous savons qu'une ellipse N'EST PAS un cercle? Si quelqu'un fait cela, il n'a pas une compréhension correcte des entités qu'il essaie de modéliser. Mais en permettant cette substitution, n'encourageons-nous pas une compréhension vague du système sous-jacent que nous essayons de modéliser dans notre logiciel, et donc de créer un mauvais logiciel en effet?
franc
@maverick Je crois que vous avez lu la relation que j'ai décrite à l'envers. La relation is-a proposée est l'inverse: un cercle est une ellipse. Plus précisément, un cercle est une ellipse où les rayons alpha et bêta sont identiques. Et donc, l'attente pourrait être que toute fonction attendant une ellipse comme paramètre pourrait également prendre un cercle. Considérez calcul_area (Ellipse). Passer un cercle à cela donnerait le même résultat. Mais le problème est que le comportement des fonctions de mutation d'Ellipse n'est pas substituable à celles de Circle.
Kaz Dragon
6

Dans les mots du profane:

Votre code aura énormément de clauses CASE / switch partout.

Chacune de ces clauses CASE / switch aura besoin de nouveaux cas ajoutés de temps en temps, ce qui signifie que la base de code n'est pas aussi évolutive et maintenable qu'elle devrait l'être.

LSP permet au code de fonctionner plus comme du matériel:

Vous n'avez pas à modifier votre iPod car vous avez acheté une nouvelle paire de haut-parleurs externes, car les anciens et les nouveaux haut-parleurs externes respectent la même interface, ils sont interchangeables sans que l'iPod ne perde la fonctionnalité souhaitée.

Tulains Córdova
la source
2
-1: mauvaise réponse
Thomas Eding
3
@Thomas, je ne suis pas d'accord. C'est une bonne analogie. Il parle de ne pas briser les attentes, c'est la raison d'être du LSP. (bien que la partie sur le boîtier / commutateur soit un peu faible, je suis d'accord)
Andres F.
2
Et puis Apple a cassé le LSP en changeant les connecteurs. Cette réponse perdure.
Magus
Je ne comprends pas ce que les instructions de commutateur ont à voir avec LSP. si vous faites référence au basculement typeof(someObject)pour décider de ce que vous êtes "autorisé à faire", alors bien sûr, mais c'est un autre anti-modèle entièrement.
sara
Une réduction drastique du nombre d'instructions de commutation est un effet secondaire souhaitable du LSP. Comme les objets peuvent représenter tout autre objet qui étend la même interface, pas besoin de prendre en charge des cas particuliers.
Tulains Córdova
1

pour donner un exemple réel avec UndoManager de java

il hérite du AbstractUndoableEditcontrat dont il spécifie qu'il a 2 états (annulé et refait) et peut passer de l'un à l'autre avec des appels uniques vers undo()etredo()

cependant UndoManager a plus d'états et agit comme un tampon d'annulation (chaque appel pour annuler undocertaines mais pas toutes les modifications, affaiblissant la postcondition)

cela conduit à la situation hypothétique où vous ajoutez un UndoManager à un CompoundEdit avant d'appeler end()puis d'appeler annuler sur ce CompoundEdit le mènera à appeler undo()à chaque modification une fois vos modifications partiellement annulées, laissant

J'ai roulé le mien UndoManagerpour éviter cela (je devrais probablement le renommer UndoBuffercependant)

monstre à cliquet
la source
1

Exemple: vous travaillez avec une infrastructure d'interface utilisateur et vous créez votre propre contrôle d'interface utilisateur personnalisé en sous-classant la Controlclasse de base. La Controlclasse de base définit une méthode getSubControls()qui doit renvoyer une collection de contrôles imbriqués (le cas échéant). Mais vous remplacez la méthode pour renvoyer réellement une liste des dates de naissance des présidents des États-Unis.

Alors, qu'est-ce qui peut mal tourner avec ça? Il est évident que le rendu du contrôle échouera, car vous ne renvoyez pas une liste de contrôles comme prévu. L'interface utilisateur se bloquera très probablement. Vous rompez le contrat auquel les sous-classes de contrôle doivent adhérer.

JacquesB
la source
0

Vous pouvez également le regarder du point de vue de la modélisation. Lorsque vous dites qu'une instance de classe Aest également une instance de classe, Bvous impliquez que "le comportement observable d'une instance de classe Apeut également être classé comme comportement observable d'une instance de classe B" (ceci n'est possible que si la classe Best moins spécifique que classe A.)

Donc, violer le LSP signifie qu'il y a une contradiction dans votre conception: vous définissez des catégories pour vos objets et ensuite vous ne les respectez pas dans votre implémentation, quelque chose doit être faux.

Comme faire une boîte avec une étiquette: "Cette boîte ne contient que des boules bleues", puis y jeter une boule rouge. À quoi sert une telle étiquette si elle affiche des informations erronées?

Giorgio
la source
0

J'ai récemment hérité d'une base de code qui contient quelques violateurs majeurs de Liskov. Dans les classes importantes. Cela m'a causé énormément de douleur. Laissez-moi vous expliquer pourquoi.

J'ai Class A, qui dérive de Class B. Class Aet Class Bpartager un tas de propriétés qui Class Aremplace avec sa propre implémentation. La définition ou l'obtention d'une Class Apropriété a un effet différent de la définition ou de l'obtention de la même propriété exacte Class B.

public Class A
{
    public virtual string Name
    {
        get; set;
    }
}

Class B : A
{
    public override string Name
    {
        get
        {
            return TranslateName(base.Name);
        }
        set
        {
            base.Name = value;
            FunctionWithSideEffects();
        }
    }
}

Mis à part le fait qu'il s'agit d'une façon tout à fait terrible de faire la traduction dans .NET, il existe un certain nombre d'autres problèmes avec ce code.

Dans ce cas, Nameest utilisé comme index et variable de contrôle de flux à plusieurs endroits. Les classes ci-dessus sont jonchées dans toute la base de code sous leur forme brute et dérivée. Violer le principe de substitution de Liskov dans ce cas signifie que j'ai besoin de connaître le contexte de chaque appel unique à chacune des fonctions qui prennent la classe de base.

Le code utilise des objets des deux Class Aet Class B, donc je ne peux pas simplement faire un Class Arésumé pour forcer les gens à l'utiliser Class B.

Il existe des fonctions utilitaires très utiles qui fonctionnent Class Aet d'autres fonctions utilitaires très utiles qui fonctionnent Class B. Idéalement , je voudrais être en mesure d'utiliser une fonction utilitaire qui peut fonctionner sur Class Ale Class B. De nombreuses fonctions qui prennent un Class Bpourraient facilement prendre un Class Asans la violation du LSP.

La pire chose à ce sujet est que ce cas particulier est vraiment difficile à refactoriser car l'application entière dépend de ces deux classes, fonctionne tout le temps sur les deux classes et se briserait d'une centaine de manières si je change cela (ce que je vais faire en tous cas).

Ce que je devrai faire pour résoudre ce problème est de créer une NameTranslatedpropriété, qui sera la Class Bversion de la Namepropriété et de modifier très, très soigneusement chaque référence à la Namepropriété dérivée pour utiliser ma nouvelle NameTranslatedpropriété. Cependant, même si l'une de ces références est erronée, l'application entière pourrait exploser.

Étant donné que la base de code n'a pas de tests unitaires autour d'elle, cela est assez proche d'être le scénario le plus dangereux auquel un développeur peut être confronté. Si je ne change pas la violation, je dois dépenser d'énormes quantités d'énergie mentale pour suivre le type d'objet utilisé dans chaque méthode et si je corrige la violation, je pourrais faire exploser le produit entier à un moment inopportun.

Stephen
la source
Que se passerait-il si, dans la classe dérivée, vous observiez la propriété héritée avec un autre type de chose qui avait le même nom [par exemple une classe imbriquée] et créiez de nouveaux identificateurs BaseNameet TranslatedNamepour accéder à la fois au style de classe A Nameet à la signification de classe B? Ensuite, toute tentative d'accès Namesur une variable de type Bserait rejetée avec une erreur de compilation, vous pouvez donc vous assurer que toutes les références ont été converties dans l'un des autres formulaires.
supercat
Je ne travaille plus à cet endroit. Il aurait été très difficile à corriger. :-)
Stephen
-4

Si vous voulez ressentir le problème de la violation de LSP, pensez à ce qui se passe si vous n'avez que .dll / .jar de classe de base (pas de code source) et que vous devez créer une nouvelle classe dérivée. Vous ne pouvez jamais terminer cette tâche.

sgud
la source
1
Cela ouvre simplement plus de questions plutôt que d'être une réponse.
Frank