1190 votes

Quel est un exemple du principe de substitution de Liskov ?

J'ai entendu dire que le principe de substitution de Liskov (LSP) est un principe fondamental de la conception orientée objet. Qu'est-ce que c'est et quels sont les exemples de son utilisation ?

0 votes

Autres exemples d'adhésion et de violation des FSL aquí

1 votes

Cette question a une infinité de bonnes réponses et est donc trop vaste .

0 votes

12 ans plus tard, il serait formidable que vous puissiez annuler votre propre réponse et en accepter une autre, puisque votre propre réponse est malheureusement la suivante complètement faux et ne décrit pas un LSP mais plutôt une erreur d'interface. Et, depuis qu'elle est acceptée, elle induit beaucoup de gens en erreur (comme l'indique le nombre de votes positifs).

1180voto

m-sharp Points 4349

Un excellent exemple illustrant les PSL (donné par Oncle Bob dans un podcast que j'ai écouté récemment) est le fait que, parfois, quelque chose qui semble correct en langage naturel ne fonctionne pas tout à fait en code.

En mathématiques, un Square est un Rectangle . En effet, c'est une spécialisation d'un rectangle. Le "est un" donne envie de modéliser cela avec de l'héritage. Cependant si dans le code vous avez fait Square découler de Rectangle alors a Square devrait être utilisable partout où vous attendez un Rectangle . Cela donne lieu à un comportement étrange.

Imaginez que vous ayez SetWidth y SetHeight sur votre Rectangle classe de base ; cela semble parfaitement logique. Cependant, si votre Rectangle pointant vers une référence Square entonces SetWidth y SetHeight n'a pas de sens, car le fait d'en définir un modifierait l'autre pour l'adapter. Dans ce cas Square échoue au test de substitution de Liskov avec Rectangle et l'abstraction d'avoir Square hériter de Rectangle est une mauvaise chose.

enter image description here

Vous devriez tous vérifier l'autre inestimable Les principes de SOLID expliqués par des affiches de motivation .

28 votes

@m-sharp Et si c'était un Rectangle immuable tel qu'au lieu de SetWidth et SetHeight, nous avons les méthodes GetWidth et GetHeight à la place ?

185 votes

Morale de l'histoire : modélisez vos classes en fonction des comportements et non des propriétés ; modélisez vos données en fonction des propriétés et non des comportements. Si cela se comporte comme un canard, c'est certainement un oiseau.

266 votes

Eh bien, un carré est clairement un type de rectangle dans le monde réel. La possibilité de modéliser cela dans notre code dépend de la spécification. Ce que la LSP indique, c'est que le comportement du sous-type doit correspondre au comportement du type de base tel que défini dans la spécification du type de base. Si la spécification du type de base rectangle indique que la hauteur et la largeur peuvent être définies indépendamment, alors la LSP indique que le carré ne peut pas être un sous-type de rectangle. Si la spécification du type rectangle indique qu'un rectangle est immuable, alors un carré peut être un sous-type de rectangle. Il s'agit de faire en sorte que les sous-types conservent le comportement spécifié pour le type de base.

542voto

NotMyself Points 7567

Le principe de substitution de Liskov (LSP, lsp ) est un concept de la programmation orientée objet qui stipule :

Les fonctions qui utilisent des pointeurs ou références à des classes de base doivent être capables d'utiliser des objets de classes dérivées sans le savoir.

Au cœur des PSL se trouvent les interfaces et les contrats, ainsi que la manière de décider quand étendre une classe ou utiliser une autre stratégie telle que la composition pour atteindre votre objectif.

La manière la plus efficace que j'ai vue pour illustrer ce point était dans La tête haute OOA&D . Ils présentent un scénario dans lequel vous êtes un développeur sur un projet visant à construire un cadre pour les jeux de stratégie.

Ils présentent une classe qui représente un tableau qui ressemble à ceci :

Class Diagram

Toutes les méthodes prennent les coordonnées X et Y comme paramètres pour localiser la position de la tuile dans le tableau bidimensionnel de Tiles . Cela permettra à un développeur de jeux de gérer les unités dans le tableau au cours du jeu.

Le livre modifie ensuite les exigences en stipulant que le cadre de jeu doit également prendre en charge les cartes de jeu en 3D afin de permettre aux jeux de voler. Ainsi, un ThreeDBoard est introduite et étend la classe Board .

À première vue, cela semble être une bonne décision. Board fournit à la fois le Height y Width propriétés et ThreeDBoard fournit l'axe Z.

Là où ça se casse la figure, c'est quand vous regardez tous les autres membres hérités de Board . Les méthodes de AddUnit , GetTile , GetUnits et ainsi de suite, prennent tous les paramètres X et Y dans le format Board mais la classe ThreeDBoard a également besoin d'un paramètre Z.

Vous devez donc implémenter à nouveau ces méthodes avec un paramètre Z. Le paramètre Z n'a aucun lien avec le Board et les méthodes héritées de la classe Board classe perdent leur sens. Une unité de code qui tente d'utiliser la classe ThreeDBoard comme classe de base Board serait très malchanceux.

On devrait peut-être trouver une autre approche. Au lieu d'étendre Board , ThreeDBoard devrait être composé de Board objets. Un site Board objet par unité de l'axe Z.

Cela nous permet d'utiliser les bons principes de l'orientation objet, comme l'encapsulation et la réutilisation, et ne viole pas la LSP.

13 votes

Voir aussi Problème de cercle et d'ellipse sur Wikipedia pour un exemple similaire mais plus simple.

0 votes

Requote de @NotMySelf : "Je pense que l'exemple est simplement pour démontrer que l'héritage de board n'a pas de sens dans le contexte de ThreeDBoard et que toutes les signatures de méthodes n'ont pas de sens avec un axe Z.".

0 votes

Requote de @Chris Ammerman : "L'évaluation de l'adhésion aux PSL peut être un excellent outil pour déterminer quand la composition est le mécanisme le plus approprié pour étendre une fonctionnalité existante, plutôt que l'héritage."

153voto

Konrad Rudolph Points 231505

LSP concerne les invariants.

L'exemple classique est donné par la déclaration pseudo-code suivante (implémentations omises) :

class Rectangle {
    int getHeight()
    void setHeight(int value) {
        postcondition: width didn’t change
    }
    int getWidth()
    void setWidth(int value) {
        postcondition: height didn’t change
    }
}

class Square extends Rectangle { }

Maintenant nous avons un problème bien que l'interface corresponde. La raison en est que nous avons violé des invariants issus de la définition mathématique des carrés et des rectangles. Selon le mode de fonctionnement des getters et setters, un Rectangle doit satisfaire l'invariant suivant :

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

Cependant, cet invariant (ainsi que les postconditions explicites) doit être violée par une mise en œuvre correcte de Square Il ne s'agit donc pas d'un substitut valable de Rectangle .

39 votes

D'où la difficulté d'utiliser "OO" pour modéliser tout ce que nous pourrions vouloir modéliser réellement.

10 votes

@DrPizza : Absolument. Cependant, deux choses. Premièrement, ces relations peuvent toujours être modélisé en POO, bien que de manière incomplète ou en utilisant des détours plus complexes (choisissez ce qui convient le mieux à votre problème). Deuxièmement, il n'y a pas de meilleure alternative. D'autres mappings/modelings ont les mêmes problèmes ou des problèmes similaires ;-)

0 votes

@KonradRudolph, C'est un bon exemple. Quelle est la bonne façon de déclarer Square alors ?

82voto

Phillip Wells Points 2625

Robert Martin a un excellent article sur le principe de substitution de Liskov . Il examine les façons subtiles et moins subtiles dont le principe peut être violé.

Quelques parties pertinentes du document (notez que le deuxième exemple est fortement condensé) :

Un exemple simple de violation du LSP

L'une des violations les plus flagrantes de ce principe est l'utilisation du langage C++. C++ pour sélectionner une fonction en fonction du type d'un objet. type d'un objet, par exemple :

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Il est clair que le DrawShape fonction est mal formée. Elle doit connaître toutes les dérivées possibles de la Shape et elle doit être modifiée chaque fois que de nouvelles dérivées de Shape sont créés. En effet, beaucoup considèrent la structure de cette fonction comme un anathème pour la conception orientée objet.

Carré et rectangle, une violation plus subtile.

Cependant, il existe d'autres façons, bien plus subtiles, de violer la LSP. Considérons une application qui utilise le Rectangle classe telle que décrite ci-dessous :

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] Imaginons qu'un jour les utilisateurs demandent à pouvoir manipuler des carrés en plus des rectangles. [...]

Il est clair qu'un carré est un rectangle à toutes fins utiles. Puisque la relation ISA se vérifie, il est logique de modéliser la relation Square comme étant dérivée de la classe Rectangle . [...]

Square héritera de la SetWidth y SetHeight fonctions. Ces fonctions sont tout à fait inappropriées pour un Square puisque la largeur et hauteur d'un carré sont identiques. Cela devrait être un indice important qu'il y a un problème avec la conception. Cependant, il existe un moyen de contourner le problème. Nous pouvons passer outre SetWidth y SetHeight [...]

Mais considérez la fonction suivante :

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Si nous passons une référence à un Square dans cette fonction, l'objet Square sera corrompu car la hauteur ne sera pas modifiée. Il s'agit d'une violation claire de la LSP. La fonction ne fonctionne pas pour les dérivés de ses arguments.

[...]

17 votes

Bien tard, mais j'ai pensé que c'était une citation intéressante dans ce journal : Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. Si une pré-condition de classe enfantine est plus forte qu'une pré-condition de classe parentale, vous ne pouvez pas substituer un enfant à un parent sans violer la pré-condition. D'où la LSP.

0 votes

@user2023861 Vous avez parfaitement raison. Je vais écrire une réponse basée sur cela.

41voto

Shelby Moore III Points 2088

LSP est nécessaire lorsqu'un code pense qu'il appelle les méthodes d'un type T et peut, sans le savoir, appeler les méthodes d'un type S , donde S extends T (c'est-à-dire S hérite, dérive ou est un sous-type du supertype T ).

Par exemple, cela se produit lorsqu'une fonction avec un paramètre d'entrée de type T est appelé (c'est-à-dire invoqué) avec une valeur d'argument de type S . Ou, lorsqu'un identifiant de type T on lui attribue une valeur de type S .

val id : T = new S() // id thinks it's a T, but is a S

LSP exige les attentes (c'est-à-dire les invariants) pour les méthodes de type T (par exemple Rectangle ), ne soit pas violée lorsque les méthodes de type S (par exemple Square ) sont appelés à la place.

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Même un type avec champs immuables possède encore des invariants, par exemple le immuable Les régleurs de rectangles s'attendent à ce que les dimensions soient modifiées de manière indépendante, mais la fonction immuable Les régleurs d'équerre violent cette attente.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP exige que chaque méthode du sous-type S doit avoir un ou plusieurs paramètres d'entrée contravariants et une sortie covariante.

Contravariant signifie que la variance est contraire à la direction de l'héritage, c'est-à-dire le type Si , de chaque paramètre d'entrée de chaque méthode du sous-type S doit être le même ou un supertype du type Ti du paramètre d'entrée correspondant de la méthode correspondante du supertype T .

La covariance signifie que la variance est dans la même direction que l'héritage, c'est-à-dire le type So , de la sortie de chaque méthode du sous-type S doit être le même ou un sous-type du type To de la sortie correspondante de la méthode correspondante de la supertype T .

En effet, si l'appelant pense avoir un type T pense qu'il appelle une méthode de T alors il fournit un ou plusieurs arguments de type Ti et assigne la sortie au type To . Lorsqu'il appelle effectivement la méthode correspondante de S alors chaque Ti L'argument d'entrée est affecté à un Si et le paramètre d'entrée So La sortie est affectée au type To . Ainsi, si Si n'étaient pas contravariants par rapport à Ti alors un sous-type Xi -qui ne serait pas un sous-type de Si -pourrait être attribué à Ti .

En outre, pour les langages (par exemple, Scala ou Ceylon) qui ont des annotations de variance de site de définition sur les paramètres de polymorphisme de type (c'est-à-dire les génériques), la co- ou la contre-direction de l'annotation de variance pour chaque paramètre de type du type T doit être en face de ou la même direction respectivement à chaque paramètre d'entrée ou sortie (de chaque méthode de T ) qui a le type du paramètre de type.

En outre, pour chaque paramètre d'entrée ou de sortie qui a un type de fonction, la direction de la variance requise est inversée. Cette règle est appliquée de manière récursive.


Le sous-typage est approprié où les invariants peuvent être énumérés.

De nombreuses recherches sont en cours sur la manière de modéliser les invariants, afin qu'ils soient appliqués par le compilateur.

Typestate (voir page 3) déclare et applique des invariants d'état orthogonaux au type. Alternativement, les invariants peuvent être appliqués par convertir les assertions en types . Par exemple, pour affirmer qu'un fichier est ouvert avant de le fermer, alors File.open() pourrait retourner un type OpenFile, qui contient une méthode close() qui n'est pas disponible dans File. A API de type "tic-tac-toe peut être un autre exemple d'utilisation du typage pour faire respecter les invariants au moment de la compilation. Le système de type peut même être Turing-complet, par ex. Scala . Les langages à typage dépendant et les prouveurs de théorèmes formalisent les modèles de typage d'ordre supérieur.

En raison du besoin de sémantique pour abstraction par rapport à l'extension Je m'attends à ce que l'emploi du typage pour modéliser les invariants, c'est-à-dire une sémantique dénotationnelle d'ordre supérieur unifiée, soit supérieur au Typestate. L'"extension" signifie la composition illimitée et permutée d'un développement modulaire et non coordonné. Parce que cela me semble être l'antithèse de l'unification et donc des degrés de liberté, d'avoir deux modèles mutuellement dépendants (par exemple, les types et le Typestate) pour exprimer la sémantique partagée, qui ne peut pas être unifié avec l'autre pour la composition extensible. Par exemple, Problème d'expression a été unifié dans les domaines du sous-typage, de la surcharge de fonctions et du typage paramétrique.

Ma position théorique est que pour la connaissance pour exister (voir section "La centralisation est aveugle et impropre"), il y aura jamais être un modèle général qui peut imposer une couverture à 100% de tous les invariants possibles dans un langage informatique complet de Turing. Pour que la connaissance existe, des possibilités inattendues doivent exister, c'est-à-dire que le désordre et l'entropie doivent toujours augmenter. C'est la force entropique. Prouver tous les calculs possibles d'une extension potentielle, c'est calculer a priori toutes les extensions possibles.

C'est pourquoi le théorème de la halte existe, c'est-à-dire qu'il est indécidable de savoir si chaque programme possible dans un langage de programmation complet de Turing se termine. Il est possible de prouver qu'un programme spécifique se termine (un programme dont toutes les possibilités ont été définies et calculées). Mais il est impossible de prouver que toutes les extensions possibles de ce programme se terminent, à moins que les possibilités d'extension de ce programme ne soient pas complètes en Turing (par exemple, via un typage dépendant). Puisque l'exigence fondamentale pour la complétude de Turing est la suivante récursion sans limite il est intuitif de comprendre comment les théorèmes d'incomplétude de Gödel et le paradoxe de Russell s'appliquent à l'extension.

Une interprétation de ces théorèmes les intègre dans une compréhension conceptuelle généralisée de la force entropique :

  • Théorèmes d'incomplétude de Gödel : toute théorie formelle, dans laquelle toutes les vérités arithmétiques peuvent être prouvées, est inconsistante.
  • Le paradoxe de Russell Chaque règle d'appartenance d'un ensemble qui peut contenir un ensemble, énumère le type spécifique de chaque membre ou se contient lui-même. Ainsi, soit les ensembles ne peuvent pas être étendus, soit ils constituent une récursion non limitée. Par exemple, l'ensemble de tout ce qui n'est pas une théière, s'inclut lui-même, qui s'inclut lui-même, qui s'inclut lui-même, etc .. Ainsi, une règle est incohérente si elle (peut contenir un ensemble et) n'énumère pas les types spécifiques (c'est-à-dire qu'elle autorise tous les types non spécifiés) et ne permet pas une extension non limitée. Il s'agit de l'ensemble des ensembles qui ne sont pas membres d'eux-mêmes. Cette incapacité à être à la fois cohérent et complètement énuméré sur toutes les extensions possibles, est le théorème d'incomplétude de Gödel.
  • Principe de substitution de Liskov : en général, c'est un problème indécidable de savoir si un ensemble est le sous-ensemble d'un autre, c'est-à-dire que l'héritage est généralement indécidable.
  • Référencement Linsky il est indécidable de savoir quel est le calcul de quelque chose, lorsqu'il est décrit ou perçu, c'est-à-dire que la perception (la réalité) n'a pas de point de référence absolu.
  • Le théorème de Coase Il n'y a pas de point de référence externe, donc toute barrière aux possibilités externes illimitées est vouée à l'échec.
  • Deuxième loi de la thermodynamique : l'univers entier (un système fermé, c'est-à-dire tout) tend vers le désordre maximal, c'est-à-dire le maximum de possibilités indépendantes.

20 votes

@Shelyby : Vous avez mélangé trop de choses. Les choses ne sont pas aussi confuses que vous le dites. La plupart de vos affirmations théoriques reposent sur des bases fragiles, comme "Pour que la connaissance existe, des possibilités inattendues doivent exister, .........". ET "généralement, c'est un problème indécidable de savoir si un ensemble est le sous-ensemble d'un autre, c'est-à-dire que l'héritage est généralement indécidable". Vous pouvez créer un blog séparé pour chacun de ces points. Quoi qu'il en soit, vos affirmations et hypothèses sont très discutables. Il ne faut pas utiliser des choses dont on n'est pas conscient !

1 votes

@aknon I avoir un blog qui explique ces questions de manière plus approfondie. Mon modèle TOE d'espace-temps infini a des fréquences non limitées. Pour moi, il n'y a pas de confusion dans le fait qu'une fonction inductive récursive ait une valeur de départ connue et une limite finale infinie, ou qu'une fonction coinductive ait une valeur finale inconnue et une limite de départ connue. La relativité est le problème une fois que la récursion est introduite. C'est pourquoi Turing complète est équivalente à une récursion non bornée .

6 votes

@ShelbyMooreIII Tu vas dans trop de directions. Ce n'est pas une réponse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X