Question Qu'est-ce qu'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 quelques exemples de son utilisation?


683
2017-09-11 15:17


origine


Réponses:


Un bon exemple illustrant LSP (donné par Oncle Bob dans un podcast que j'ai entendu récemment) était comment parfois quelque chose qui sonne correctement dans le langage naturel ne fonctionne pas tout à fait dans le code.

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

Imaginez que vous aviez SetWidth et SetHeight méthodes sur votre Rectangle classe de base; cela semble parfaitement logique. Cependant si votre Rectangle référence a souligné un Square, puis SetWidth et SetHeight Cela n'a pas de sens parce que la mise en place de l'une changerait l'autre pour la faire correspondre. Dans ce cas Square échoue le test de substitution Liskov avec Rectangle et l'abstraction d'avoir Square hériter de Rectangle est un mauvais.

Vous devriez vérifier les autres inestimables Principes de SOLID Affiches de motivation.


654
2018-02-25 04:44



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

Fonctions qui utilisent des pointeurs ou   les références aux classes de base doivent être   capable d'utiliser des objets de classes dérivées   sans le savoir.

En son cœur, LSP traite des interfaces et des contrats, ainsi que de la façon de décider quand étendre une classe ou d'utiliser une autre stratégie telle que la composition pour atteindre votre objectif.

Le moyen le plus efficace que j'ai vu pour illustrer ce point était Chef Premier OOA & D. Ils présentent un scénario où vous êtes un développeur sur un projet pour 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 à deux dimensions de Tiles. Cela permettra à un développeur de jeux de gérer les unités du plateau au cours du jeu.

Le livre continue à changer les exigences pour dire que le travail de cadre de jeu doit également soutenir les cartes de jeu 3D pour accueillir les jeux qui ont un vol. Donc un ThreeDBoard classe est introduite qui étend Board.

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

En cas de panne, c'est quand vous regardez tous les autres membres hérités de Board. Les méthodes pour AddUnit, GetTile, GetUnits et ainsi de suite, tous prennent les paramètres X et Y dans le Board classe mais le ThreeDBoard a également besoin d'un paramètre Z.

Vous devez donc réimplémenter ces méthodes avec un paramètre Z. Le paramètre Z n'a pas de contexte pour le Board classe et les méthodes héritées de la Board classe perdent leur sens. Une unité de code essayant d'utiliser le ThreeDBoardclasse comme sa classe de base Board serait très malchanceux.

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

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


385
2017-09-11 15:34



LSP concerne les invariants.

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

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

Maintenant nous avons un problème bien que l'interface corresponde. La raison en est que nous avons violé les invariants issus de la définition mathématique des carrés et des rectangles. La façon dont les getters et les setters fonctionnent, Rectangle devrait 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 doit être violé par une mise en œuvre correcte de Square, donc ce n'est pas un substitut valide de Rectangle.


110
2017-09-12 13:45



Robert Martin a un excellent document sur le principe de substitution de Liskov. Il discute des manières subtiles et pas si subtiles dans lesquelles le principe peut être violé.

Quelques parties pertinentes de l'article (notez que le second exemple est fortement condensé):

Un exemple simple d'une violation de LSP

L'une des violations les plus flagrantes de ce principe est l'utilisation de C ++   Information de type à l'exécution (RTTI) pour sélectionner une fonction basée sur le   type d'un objet. c'est à dire.:

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));
}

Clairement le DrawShape la fonction est mal formée. Il doit savoir sur   tous les dérivés possibles de la Shape classe, et il doit être changé   chaque fois que de nouveaux dérivés de Shape sont créées. En effet, beaucoup considèrent la structure de cette fonction comme un anathème à la conception orientée objet.

Square et rectangle, une violation plus subtile.

Cependant, il existe d'autres moyens, bien plus subtils, de violer le LSP.   Envisager une application qui utilise le Rectangle classe comme décrit   au dessous de:

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;
};

[...] Imaginez qu'un jour les utilisateurs exigent la possibilité de manipuler   carrés en plus des rectangles. [...]

Clairement, un carré est un rectangle pour toutes les intentions et fins normales.   Puisque la relation ISA est valide, il est logique de modéliser Square   classe comme étant dérivé de Rectangle. [...]

Square héritera du SetWidth et SetHeight les fonctions. Celles-ci   fonctions sont tout à fait inappropriées pour un Square, depuis la largeur et   la 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 pourrions passer outre SetWidth et 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 objet dans cette fonction, le    Square l'objet sera corrompu car la hauteur ne sera pas modifiée.   Ceci est une violation claire de LSP. La fonction ne fonctionne pas pour   dérivés de ses arguments.

[...]


68
2017-09-12 13:34



LSP est nécessaire quand un code pense appeler les méthodes d'un type T, et peut appeler sans le savoir les méthodes d'un type S, où S extends T (c'est à dire. S hérite, dérive de, 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 appelé) avec une valeur d'argument de type S. Ou, où un identifiant de type T, est affecté d'une valeur de type S.

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

LSP nécessite les attentes (c'est-à-dire les invariants) pour les méthodes de type T (par exemple. Rectangle), ne pas être violé 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 a toujours des invariants, par ex. la immuable Les régleurs rectangulaires s'attendent à ce que les dimensions soient modifiées indépendamment, mais immuable Les setters carrés 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 nécessite que chaque méthode du sous-type S doit avoir un (des) paramètre (s) d'entrée contravariant (s) 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 du supertype T.

C'est parce que si l'appelant pense qu'il a un type T, pense qu'il appelle une méthode de T, alors il fournit des arguments de type Ti et affecte la sortie au type To. Quand il appelle réellement la méthode correspondante de Spuis chaque Ti argument d'entrée est affecté à un Si paramètre d'entrée, et le So la sortie est affectée au type To. Donc si Si n'étaient pas contravariant w.r.t. à Ti, puis un sous-type Xi-qui ne serait pas un sous-type de Si-peut être affecté à Ti.

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

De plus, pour chaque paramètre ou sortie d'entrée ayant un type de fonction, la direction de 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.

Il y a beaucoup de recherches en cours sur la façon de modéliser les invariants, afin qu'ils soient appliqués par le compilateur.

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

En raison du besoin de sémantique abstrait sur l'extension, Je pense que l'utilisation de la typage pour modéliser les invariants, c'est-à-dire la sémantique dénotationnelle d'ordre supérieur unifiée, est supérieure à la typage. «Extension» signifie la composition illimitée et permutée d'un développement modulaire non coordonné. Parce qu'il 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 types et typage) pour exprimer la sémantique partagée, qui ne peuvent pas être unifiés les uns avec les autres pour une composition extensible . Par exemple, Problème d'expressionL'extension-like a été unifiée dans les domaines de sous-typage, de surcharge de fonctions et de typage paramétrique.

Ma position théorique est que pour la connaissance d'exister (voir la section "La centralisation est aveugle et impropre"), il y aura jamais être un modèle général qui peut imposer une couverture de 100% de tous les invariants possibles dans un langage informatique complet de Turing. Pour que la connaissance existe, il existe beaucoup de possibilités inattendues, 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 toute extension possible.

C'est pourquoi le théorème de Halting existe, c'est-à-dire qu'il est indécidable de terminer tous les programmes possibles dans un langage de programmation Turing-complete. Il peut être prouvé que certains programmes spécifiques se terminent (un programme dont toutes les possibilités ont été définies et calculées). Mais il est impossible de prouver que toute extension possible de ce programme se termine, à moins que les possibilités d'extension de ce programme ne soient pas complètes (par exemple, via le typage dépendant). Puisque l'exigence fondamentale pour Turing-exhauseness est récursivité illimitée, 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 incorpore dans une compréhension conceptuelle généralisée de la force entropique:

  • Les 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 incohérente.
  • Le paradoxe de Russell: chaque règle d'appartenance pour un ensemble pouvant contenir un ensemble, énumère le type spécifique de chaque membre ou se contient lui-même. Ainsi, les ensembles ne peuvent pas être étendus ou sont une récursion illimitée. Par exemple, l'ensemble de tout ce qui n'est pas une théière, comprend lui-même, qui comprend lui-même, qui comprend 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 autorise tous les types non spécifiés) et ne permet pas l'extension illimitée. C'est l'ensemble des ensembles qui ne sont pas membres d'eux-mêmes. Cette incapacité à être à la fois cohérente et complètement énumérée sur toute extension possible, 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 que n'importe quel ensemble soit 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 que le calcul de quelque chose soit, quand 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 échouera.
  • 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.

39
2017-11-26 16:35



La substituabilité est un principe dans la programmation orientée objet indiquant que, dans un programme d'ordinateur, si S est un sous-type de T, alors les objets de type T peuvent être remplacés par des objets de type S

Faisons un exemple simple en Java:

Mauvais exemple

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

Le canard peut voler à cause de son un oiseau, Mais qu'en est-il:

public class Ostrich extends Bird{}

Autruche est un oiseau, Mais il ne peut pas voler, la classe d'autruche est un sous-type de classe Bird, mais il ne peut pas utiliser la méthode de la mouche, ce qui signifie que nous rompons principe LSP.

Bon exemple

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

32
2017-07-04 19:58



Le LSP est une règle concernant le contrat des classes: si une classe de base satisfait un contrat, alors les classes dérivées LSP doivent également satisfaire ce contrat.

En Pseudo-python

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

satisfait LSP si chaque fois que vous appelez Foo sur un objet Derived, il donne exactement les mêmes résultats que si vous appelez Foo sur un objet Base, tant que arg est le même.


19
2017-11-08 17:53



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

Quand j'ai lu pour la première fois sur LSP, j'ai supposé que cela signifiait dans un sens très strict, l'assimilant essentiellement à l'implémentation de l'interface et à la transtypage de type sécurité. Ce qui signifierait que le LSP est assuré ou non par le langage lui-même. Par exemple, dans ce sens strict, ThreeDBoard est certainement substituable à Board, en ce qui concerne le compilateur.

Après avoir lu plus sur le concept mais j'ai trouvé que LSP est généralement interprété plus largement que cela.

En bref, ce que cela signifie pour le code client de "savoir" que l'objet derrière le pointeur est d'un type dérivé plutôt que le type pointeur n'est pas limité à type-safety. L'adhésion à LSP est également testable en sondant le comportement réel des objets. C'est-à-dire, examiner l'impact des arguments d'état et de méthode d'un objet sur les résultats des appels de méthode ou les types d'exceptions lancées à partir de l'objet.

Revenons à l'exemple, en théorie les méthodes de conseil peuvent être faites pour fonctionner très bien sur ThreeDBoard. Dans la pratique, cependant, il sera très difficile d'empêcher les différences de comportement que le client ne peut pas gérer correctement, sans bousculer les fonctionnalités que ThreeDBoard a l'intention d'ajouter.

Avec ces connaissances en main, l'évaluation de l'adhérence des LSP peut être un excellent outil pour déterminer quand la composition est le mécanisme le plus approprié pour étendre la fonctionnalité existante, plutôt que l'héritage.


18
2017-09-11 16:04



Bizarrement, personne n'a posté l'original papier cela a décrit lsp. Ce n'est pas une lecture facile comme celle de Robert Martin, mais ça vaut le coup.


16
2018-02-25 06:21