Quel est un exemple du principe de substitution de Liskov?

908

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?

Pas moi
la source
Plus d'exemples d'adhésion et de violation du LSP ici
StuartLC
1
Cette question a une infinité de bonnes réponses et est donc trop large .
Raedwald

Réponses:

892

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

En mathématiques, a Squareest a Rectangle. En effet c'est une spécialisation d'un rectangle. Le "est un" donne envie de modéliser cela avec l'héritage. Cependant, si dans le code dont vous avez Squaredérivé Rectangle, a Squaredevrait être utilisable partout où vous vous attendez a Rectangle. Cela crée un comportement étrange.

Imaginez que vous aviez SetWidthet les SetHeightméthodes de votre Rectangleclasse de base; cela semble parfaitement logique. Cependant, si votre Rectangleréférence pointait vers a Square, alors SetWidthet SetHeightn'a pas de sens car le réglage de l'un changerait l'autre pour qu'il corresponde. Dans ce cas, Squarele test de substitution Liskov échoue Rectangleet l'abstraction d'avoir Squarehérité Rectangleest mauvaise.

entrez la description de l'image ici

Vous devriez vérifier les autres affiches de motivation inestimables des principes SOLID .

m-sharp
la source
19
@ m-sharp Et si c'est un rectangle immuable tel qu'au lieu de SetWidth et SetHeight, nous avons à la place les méthodes GetWidth et GetHeight?
Pacerier
140
Morale de l'histoire: modélisez vos classes en fonction de comportements et non de propriétés; modélisez vos données en fonction des propriétés et non des comportements. S'il se comporte comme un canard, c'est certainement un oiseau.
Sklivvz
193
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 des spécifications. Ce que le 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 du rectangle indique que la hauteur et la largeur peuvent être définies indépendamment, alors LSP indique que le carré ne peut pas être un sous-type de rectangle. Si la spécification du rectangle indique qu'un rectangle est immuable, un carré peut être un sous-type de rectangle. Il s'agit de sous-types conservant le comportement spécifié pour le type de base.
SteveT
63
@Pacerier il n'y a aucun problème s'il est immuable. Le vrai problème ici est que nous ne modélisons pas des rectangles, mais plutôt des "rectangles remodelables", c'est-à-dire des rectangles dont la largeur ou la hauteur peut être modifiée après la création (et nous considérons toujours qu'il s'agit du même objet). Si nous regardons la classe rectangle de cette manière, il est clair qu'un carré n'est pas un "rectangle remodelable", car un carré ne peut pas être remodelé et reste un carré (en général). Mathématiquement, nous ne voyons pas le problème car la mutabilité n'a même pas de sens dans un contexte mathématique.
asmeurer
14
J'ai une question sur le principe. Pourquoi serait le problème si Square.setWidth(int width)était implémenté comme ceci this.width = width; this.height = width;:? Dans ce cas, il est garanti que la largeur est égale à la hauteur.
MC Emperor
488

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

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.

Le cœur du LSP concerne les interfaces et les contrats ainsi que la façon de décider quand étendre une classe ou utiliser une autre stratégie telle que la composition pour atteindre votre objectif.

La façon la plus efficace que je l' ai vu pour illustrer ce point était Head First 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:

Diagramme de classe

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 jeu de gérer les unités du plateau au cours du jeu.

Le livre modifie ensuite les exigences pour dire que le cadre de jeu doit également prendre en charge les plateaux de jeu 3D pour accueillir les jeux qui ont un vol. Donc, une ThreeDBoardclasse est introduite qui s'étend Board.

À première vue, cela semble être une bonne décision. Boardfournit à la fois le Heightet Widthpropriétés et ThreeDBoardfournit l'axe Z.

Où il tombe en panne, c'est quand vous regardez tous les autres membres hérités Board. Les méthodes pour AddUnit, GetTile, GetUnitset ainsi de suite, toutes les deux paramètres X et Y dans la Boardclasse mais a ThreeDBoardbesoin d' un paramètre Z ainsi.

Vous devez donc implémenter à nouveau ces méthodes avec un paramètre Z. Le paramètre Z n'a pas de contexte pour la Boardclasse et les méthodes héritées de la Boardclasse perdent leur signification. Une unité de code tentant d'utiliser la ThreeDBoardclasse comme classe de base Boardserait très malchanceuse.

Nous devrions peut-être trouver une autre approche. Au lieu de s'étendre Board, ThreeDBoarddevrait être composé d' Boardobjets. Un Boardobjet 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 le LSP.

Pas moi
la source
10
Voir aussi Problème Circle-Ellipse sur Wikipedia pour un exemple similaire mais plus simple.
Brian
Citation de @NotMySelf: "Je pense que l'exemple est simplement de démontrer que l'héritage de board n'a pas de sens dans le contexte de ThreeDBoard et toutes les signatures de méthode n'ont aucun sens avec un axe Z".
Contango
1
Donc, si nous ajoutons une autre méthode à une classe Child mais que toutes les fonctionnalités de Parent ont toujours du sens dans la classe Child, cela casserait-il le LSP? Étant donné que d'une part, nous avons modifié un peu l'interface pour utiliser l'enfant d'un autre côté, si nous convertissons l'enfant en parent, le code qui attend qu'un parent fonctionne correctement.
Nickolay Kondratyev
5
Ceci est un exemple anti-Liskov. Liskov nous fait dériver le rectangle du carré. Plus de paramètres-classe de moins-paramètres-classe. Et vous avez bien montré que c'est mauvais. C'est vraiment une bonne blague d'avoir marqué comme réponse et d'avoir été voté 200 fois une réponse anti-liskov pour la question liskov. Le principe de Liskov est-il vraiment une erreur?
Gangnus
3
J'ai vu l'héritage fonctionner dans le mauvais sens. Voici un exemple. La classe de base doit être 3DBoard et la classe dérivée Board. La carte a toujours un axe Z de Max (Z) = Min (Z) = 1
Paulustrious
169

La substituabilité est un principe de la programmation orientée objet stipulant que, dans un programme informatique, 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 c'est un oiseau, mais qu'en est-il:

public class Ostrich extends Bird{}

L'autruche est un oiseau, mais elle ne peut pas voler, la classe d'autruche est un sous-type de la classe Oiseau, mais elle ne peut pas utiliser la méthode de la mouche, ce qui signifie que nous brisons le 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{} 
Maysara Alhindi
la source
3
Bel exemple, mais que feriez-vous si le client en avait Bird bird. Vous devez lancer l'objet sur FlyingBirds pour utiliser fly, ce qui n'est pas bien non?
Moody
17
Non. Si le client l'a Bird bird, cela signifie qu'il ne peut pas l'utiliser fly(). C'est ça. Passer un Duckne change rien à ce fait. Si le client aFlyingBirds bird , alors même s'il est adopté, Duckil devrait toujours fonctionner de la même manière.
Steve Chamaillard
9
Cela ne servirait-il pas également de bon exemple pour la séparation d'interfaces?
Saharsh
Excellent exemple Merci Man
Abdelhadi Abdo
6
Que diriez-vous d'utiliser l'interface «Flyable» (ne peut pas penser à un meilleur nom). De cette façon, nous ne nous engageons pas dans cette hiérarchie rigide. Sauf si nous savons vraiment en avoir besoin.
Thirdy
132

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

Nous avons maintenant 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 setters fonctionnent, Rectangledevrait 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 deSquare , il n'est donc pas un substitut valide de Rectangle.

Konrad Rudolph
la source
35
D'où la difficulté d'utiliser "OO" pour modéliser tout ce que nous pourrions réellement modéliser.
DrPizza
9
@DrPizza: Absolument. Cependant, deux choses. Tout d'abord, ces relations peuvent toujours être modélisées dans la POO, bien que de manière incomplète ou en utilisant des détours plus complexes (choisissez celui qui convient à votre problème). Deuxièmement, il n'y a pas de meilleure alternative. D'autres mappages / modélisations ont des problèmes identiques ou similaires. ;-)
Konrad Rudolph
7
@NickW Dans certains cas (mais pas dans ce qui précède), vous pouvez simplement inverser la chaîne d'héritage - logiquement, un point 2D est un point 3D, où la troisième dimension est ignorée (ou 0 - tous les points se trouvent sur le même plan dans Espace 3D). Mais ce n'est bien sûr pas vraiment pratique. En général, c'est l'un des cas où l'héritage n'aide pas vraiment, et aucune relation naturelle n'existe entre les entités. Modélisez-les séparément (au moins je ne connais pas de meilleure façon).
Konrad Rudolph
7
La POO est destinée à modéliser les comportements et non les données. Vos cours violent l'encapsulation avant même de violer LSP.
Sklivvz
2
@AustinWBryan Yep; plus je travaille dans ce domaine depuis longtemps, plus j'ai tendance à utiliser l'héritage pour les interfaces et les classes de base abstraites uniquement, et la composition pour le reste. C'est parfois un peu plus de travail (en ce qui concerne la frappe), mais cela évite tout un tas de problèmes et est largement repris par les autres programmeurs expérimentés.
Konrad Rudolph
77

Robert Martin a un excellent article sur le principe de substitution de Liskov . Il examine les manières subtiles et pas si subtiles dont le principe peut être violé.

Quelques parties pertinentes de l'article (notez que le deuxième 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 des informations de type d'exécution C ++ (RTTI) pour sélectionner une fonction en fonction du 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));
}

De toute évidence, la DrawShapefonction est mal formée. Il doit connaître tous les dérivés possibles de la Shapeclasse, et il doit être modifié chaque fois que de nouveaux dérivés de Shapesont 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, beaucoup plus subtiles, de violer le LSP. Considérez une application qui utilise la Rectangleclasse comme décrit 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;
};

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

De toute évidence, un carré est un rectangle à toutes fins utiles. Étant donné que la relation ISA est valide, il est logique de modéliser la Square classe comme étant dérivée de Rectangle. [...]

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

Mais considérez la fonction suivante:

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

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

[...]

Phillip Wells
la source
14
Bien en retard, mais je pensais que c'était une citation intéressante dans cet article: 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 condition préalable de classe enfant est plus forte qu'une condition préalable de classe parent, vous ne pouvez pas substituer un enfant à un parent sans violer la condition préalable. D'où LSP.
user2023861
@ user2023861 Vous avez parfaitement raison. J'écrirai une réponse sur cette base.
inf3rno
40

LSP est nécessaire lorsque certains codes pensent appeler les méthodes d'un type Tet peuvent, sans le savoir, appeler les méthodes d'un type S, où S extends T(c'est-à-dire Shé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 Test appelée (c'est-à-dire invoquée) avec une valeur d'argument de type S. Ou, lorsqu'un identifiant de type Tse voit attribuer une valeur de type S.

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

LSP nécessite que les attentes (c'est-à-dire les invariants) pour les méthodes de type T(par exemple Rectangle) ne soient pas violées lorsque les méthodes de type S(par exemple Square) sont appelées à 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 des champs immuables a toujours des invariants, par exemple les poseurs Rectangle immuables s'attendent à ce que les dimensions soient modifiées indépendamment, mais les poseurs Square immuables 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 requiert que chaque méthode du sous-type Sait des paramètres d'entrée contravariants et une sortie covariante.

Des moyens contravariantes la variance est contraire à la direction de la succession, à savoir le type Side chaque paramètre d'entrée de chaque méthode du sous - type S, doit être le même ou un supertype du type Tidu paramètre d'entrée correspondant de la méthode correspondante du supertype T.

La covariance signifie que la variance va dans le même sens que l'héritage, c'est-à-dire que le type Sode la sortie de chaque méthode du sous-type Sdoit être le même ou un sous -type Todu type de la sortie correspondante de la méthode correspondante du supertype T.

En effet, si l'appelant pense qu'il a un type T, pense qu'il appelle une méthode de T, il fournit alors des arguments de type Tiet affecte la sortie au type To. Lorsqu'il appelle réellement la méthode correspondante de S, chaque Tiargument d'entrée est affecté à un Siparamètre d'entrée et la Sosortie est affectée au type To. Ainsi, s'ils Sin'étaient pas contravariants Ti, alors un sous-type Xi- qui ne serait pas un sous-type de - Sipourrait être attribué Ti.

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

De plus, pour chaque paramètre d'entrée ou sortie ayant un type de fonction, la direction de variance requise est inversée. Cette règle est appliquée récursivement.


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

Il existe de nombreuses recherches en cours sur la façon 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 en convertissant les 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 saisie pour appliquer des invariants au moment de la compilation. Le système de type peut même être complet de Turing, par exemple Scala . Les langages et les démonstrateurs de théorèmes dépendants formalisent les modèles de typage d'ordre supérieur.

En raison du besoin de la sémantique d' abstraire sur l'extension , je m'attends à ce que l'utilisation de typage pour modéliser les invariants, c'est-à-dire la sémantique dénotationnelle unifiée d'ordre supérieur, soit supérieure à Typestate. «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 Typestate) pour exprimer la sémantique partagée, qui ne peuvent pas être unifiés entre eux pour une composition extensible . Par exemple, l' extension de type Problème d'expression 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 que la connaissance existe (voir la section «La centralisation est aveugle et impropre»), il n'y aura jamais de modèle général qui puisse appliquer une couverture à 100% de tous les invariants possibles dans un langage informatique complet de Turing. Pour que la connaissance existe, il existe de nombreuses possibilités inattendues, c'est-à-dire que le désordre et l'entropie doivent toujours augmenter. Ceci 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 Halting existe, c'est-à-dire qu'il est indécidable que tous les programmes possibles dans un langage de programmation complet de Turing se terminent. Il peut être prouvé que certains programmes spécifiques se terminent (un 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 une saisie dépendante). Puisque l'exigence fondamentale de complétude de Turing est une récursion 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 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 incohérente.
  • Paradoxe de Russell : chaque règle d'appartenance à un ensemble pouvant contenir un ensemble, énumère le type spécifique de chaque membre ou se contient. Ainsi, les ensembles ne peuvent pas être étendus ou ils sont une récursion illimitée. Par exemple, l'ensemble de tout ce qui n'est pas une théière, se comprend, se comprend, se comprend, 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 n'autorise pas l'extension illimitée. Il s'agit de l'ensemble d'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 Substition de Liskov : 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érédité est généralement indécidable.
  • Référencement Linsky : il est indécidable ce qu'est le calcul de quelque chose, quand il est décrit ou perçu, c'est-à-dire que la perception (réalité) n'a pas de point de référence absolu.
  • 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.
  • Seconde 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.
Shelby Moore III
la source
17
@Shelyby: Vous avez mélangé trop de choses. Les choses ne sont pas aussi confuses que vous les dites. Une grande partie de vos affirmations théoriques reposent sur des motifs fragiles, comme «Pour que la connaissance existe, des possibilités inattendues existent, .........» ET «en général, il est indécidable de savoir si un ensemble est le sous-ensemble d'un autre, c'est-à-dire l'hérédité est généralement indécidable ». Vous pouvez créer un blog distinct pour chacun de ces points. Quoi qu'il en soit, vos affirmations et hypothèses sont hautement discutables. Il ne faut pas utiliser des choses dont on n'a pas conscience!
aknon
1
@aknon J'ai un blog qui explique ces questions plus en profondeur. Mon modèle TOE d'espace-temps infini est des fréquences illimitées. Cela ne me dérange pas qu'une fonction inductive récursive ait une valeur de début connue avec une limite de fin infinie, ou qu'une fonction coinductive ait une valeur de fin inconnue et une limite de début connue. La relativité est le problème une fois la récursion introduite. C'est pourquoi Turing complet équivaut à une récursion illimitée .
Shelby Moore III
4
@ShelbyMooreIII Vous allez dans trop de directions. Ce n'est pas une réponse.
Soldalma
1
@Soldalma c'est une réponse. Ne le voyez-vous pas dans la section Réponse. Le vôtre est un commentaire car il est dans la section des commentaires.
Shelby Moore III
1
Comme votre mix avec Scala World!
Ehsan M. Kermani
24

Je vois des rectangles et des carrés dans chaque réponse, et comment violer le LSP.

Je voudrais montrer comment le LSP peut être conforme à un exemple réel:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

Cette conception est conforme au LSP car le comportement reste inchangé quelle que soit l'implémentation que nous choisissons d'utiliser.

Et oui, vous pouvez violer LSP dans cette configuration en faisant un simple changement comme ceci:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

Désormais, les sous-types ne peuvent plus être utilisés de la même manière car ils ne produisent plus le même résultat.

Steve Chamaillard
la source
6
L'exemple ne viole pas LSP uniquement tant que nous limitons la sémantique de Database::selectQuerypour prendre en charge uniquement le sous-ensemble de SQL pris en charge par tous les moteurs de base de données. Ce n'est guère pratique ... Cela dit, l'exemple est toujours plus facile à saisir que la plupart des autres utilisés ici.
Palec
5
J'ai trouvé cette réponse la plus facile à saisir du reste.
Malcolm Salvador
23

Il y a une liste de contrôle pour déterminer si vous violez ou non Liskov.

  • Si vous violez l'un des éléments suivants -> vous violez Liskov.
  • Si vous n'en violez pas -> je ne peux rien conclure.

Liste de contrôle:

  • Aucune nouvelle exception ne doit être levée dans la classe dérivée : si votre classe de base a lancé ArgumentNullException, vos sous-classes n'ont été autorisées qu'à lever des exceptions de type ArgumentNullException ou toute exception dérivée de ArgumentNullException. Lancer IndexOutOfRangeException est une violation de Liskov.
  • Les conditions préalables ne peuvent pas être renforcées : supposez que votre classe de base fonctionne avec un membre int. Maintenant, votre sous-type nécessite que int soit positif. Ceci est des conditions préalables renforcées, et maintenant tout code qui fonctionnait parfaitement bien avant avec des nombres négatifs est cassé.
  • Les post-conditions ne peuvent pas être affaiblies : supposez que votre classe de base requise, toutes les connexions à la base de données doivent être fermées avant le retour de la méthode. Dans votre sous-classe, vous avez outrepassé cette méthode et laissé la connexion ouverte pour une réutilisation ultérieure. Vous avez affaibli les post-conditions de cette méthode.
  • Les invariants doivent être préservés : la contrainte la plus difficile et douloureuse à respecter. Les invariants sont quelque temps cachés dans la classe de base et la seule façon de les révéler est de lire le code de la classe de base. Fondamentalement, vous devez être sûr que lorsque vous substituez une méthode, tout ce qui ne peut pas être changé doit rester inchangé après l'exécution de votre méthode substituée. La meilleure chose à laquelle je peux penser est d'imposer ces contraintes invariantes dans la classe de base mais ce ne serait pas facile.
  • Contrainte historique : lors de la substitution d'une méthode, vous n'êtes pas autorisé à modifier une propriété non modifiable dans la classe de base. Jetez un oeil à ces codes et vous pouvez voir que le nom est défini comme non modifiable (ensemble privé) mais SubType introduit une nouvelle méthode qui permet de le modifier (par réflexion):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

Il existe 2 autres éléments: Contravariance des arguments de méthode et Covariance des types de retour . Mais ce n'est pas possible en C # (je suis développeur C #) donc je m'en fous.

Référence:

Cù Đức Hiếu
la source
Je suis également développeur C # et je dirai que votre dernière déclaration n'est pas vraie à partir de Visual Studio 2010, avec le framework .Net 4.0. La covariance des types de retour permet un type de retour plus dérivé que celui défini par l'interface. Exemple: Exemple: IEnumerable <T> (T est covariant) IEnumerator <T> (T est covariant) IQueryable <T> (T est covariant) IGrouping <TKey, TElement> (TKey et TElement sont covariant) IComparer <T> (T est contravariant) IEqualityComparer <T> (T est contravariant) IComparable <T> (T est contravariant) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx
LCarter
1
Excellente réponse ciblée (bien que les questions originales portaient davantage sur des exemples que sur des règles).
Mike
22

Le LSP est une règle concernant le contrat des classes: si une classe de base satisfait un contrat, les classes dérivées du 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 dérivé, il donne exactement les mêmes résultats que l'appel de Foo sur un objet Base, tant que arg est le même.

Charlie Martin
la source
9
Mais ... si vous obtenez toujours le même comportement, alors quel est l'intérêt d'avoir la classe dérivée?
Leonid
2
Vous avez raté un point: c'est le même comportement observé . Vous pouvez, par exemple, remplacer quelque chose par des performances O (n) par quelque chose de fonctionnellement équivalent, mais par des performances O (lg n). Ou vous pouvez remplacer quelque chose qui accède aux données implémentées avec MySQL et le remplacer par une base de données en mémoire.
Charlie Martin
@Charlie Martin, codant vers une interface plutôt qu'une implémentation - je creuse ça. Ce n'est pas unique à la POO; des langages fonctionnels tels que Clojure en font également la promotion. Même en termes de Java ou de C #, je pense que l'utilisation d'une interface plutôt que l'utilisation d'une classe abstraite plus des hiérarchies de classes serait naturelle pour les exemples que vous fournissez. Python n'est pas fortement typé et n'a pas vraiment d'interfaces, du moins pas explicitement. Ma difficulté est que je fais de la POO depuis plusieurs années sans adhérer à SOLID. Maintenant que je l'ai rencontré, cela semble limitant et presque contradictoire.
Hamish Grubijan
Eh bien, vous devez revenir en arrière et vérifier le document original de Barbara. reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps Ce n'est pas vraiment énoncé en termes d'interfaces, et c'est une relation logique qui tient (ou non) dans tout langage de programmation qui a une forme d'héritage.
Charlie Martin
1
@HamishGrubijan Je ne sais pas qui vous a dit que Python n'est pas fortement tapé, mais ils vous mentaient (et si vous ne me croyez pas, lancez un interprète Python et essayez 2 + "2"). Peut-être confondez-vous «fortement tapé» avec «tapé statiquement»?
asmeurer
21

Pour faire court, laissons les rectangles rectangles et carrés carrés, exemple pratique lors de l'extension d'une classe parent, vous devez soit PRÉSERVER l'API parent exacte, soit l'étendre.

Supposons que vous ayez un référentiel ItemsRepository de base .

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

Et une sous-classe qui l'étend:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Ensuite, un client pourrait travailler avec l'API Base ItemsRepository et s'y fier.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

Le LSP est rompu lorsque le remplacement de la classe parent par une sous-classe rompt le contrat de l'API .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Vous pouvez en savoir plus sur l'écriture de logiciels maintenables dans mon cours: https://www.udemy.com/enterprise-php/

Lukas Lukac
la source
20

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.

Lorsque 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 d'interface et à la conversion de type sécurisé. Ce qui signifierait que le LSP est assuré ou non par la langue elle-même. Par exemple, au sens strict, ThreeDBoard est certainement substituable à Board, en ce qui concerne le compilateur.

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

En bref, ce que 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 de pointeur n'est pas limité à la sécurité de type. L'adhésion au LSP est également testable en testant 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 levées à partir de l'objet.

Pour revenir à l'exemple, en théorie, les méthodes de la carte peuvent fonctionner correctement sur ThreeDBoard. Dans la pratique cependant, il sera très difficile d'empêcher les différences de comportement que le client peut ne pas gérer correctement, sans entraver les fonctionnalités que ThreeDBoard est censé ajouter.

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

Chris Ammerman
la source
19

Je suppose que tout le monde a en quelque sorte couvert ce qu'est le LSP sur le plan technique: vous voulez essentiellement pouvoir vous abstenir des détails des sous-types et utiliser les supertypes en toute sécurité.

Donc Liskov a 3 règles sous-jacentes:

  1. Règle de signature: il doit y avoir une implémentation valide de chaque opération du supertype dans le sous-type syntaxiquement. Quelque chose qu'un compilateur pourra vérifier pour vous. Il existe une petite règle pour lever moins d'exceptions et être au moins aussi accessible que les méthodes de supertype.

  2. Méthodes Règle: La mise en œuvre de ces opérations est sémantiquement saine.

    • Préconditions plus faibles: les fonctions de sous-type devraient prendre au moins ce que le supertype a pris en entrée, sinon plus.
    • Postconditions plus fortes: elles devraient produire un sous-ensemble de la sortie produite par les méthodes de supertype.
  3. Règle des propriétés: cela va au-delà des appels de fonction individuels.

    • Invariants: Les choses qui sont toujours vraies doivent rester vraies. Par exemple. la taille d'un ensemble n'est jamais négative.
    • Propriétés évolutives: généralement quelque chose à voir avec l'immuabilité ou le type d'états dans lesquels l'objet peut se trouver.

Toutes ces propriétés doivent être préservées et la fonctionnalité de sous-type supplémentaire ne doit pas violer les propriétés de supertype.

Si ces trois choses sont prises en compte, vous vous êtes abstenu de la substance sous-jacente et vous écrivez du code faiblement couplé.

Source: Développement de programmes à Java - Barbara Liskov

snagpaul
la source
18

Un exemple important de l' utilisation de LSP est dans les tests de logiciels .

Si j'ai une classe A qui est une sous-classe conforme à LSP de B, alors je peux réutiliser la suite de tests de B pour tester A.

Pour tester entièrement la sous-classe A, j'ai probablement besoin d'ajouter quelques cas de test supplémentaires, mais au minimum je peux réutiliser tous les cas de test de la superclasse B.

Une façon de le réaliser est de construire ce que McGregor appelle une "hiérarchie parallèle pour les tests": ma ATestclasse héritera de BTest. Une certaine forme d'injection est alors nécessaire pour garantir que le scénario de test fonctionne avec des objets de type A plutôt que de type B (un modèle de méthode de modèle simple fera l'affaire).

Notez que la réutilisation de la suite de super-tests pour toutes les implémentations de sous-classe est en fait un moyen de tester que ces implémentations de sous-classe sont conformes au LSP. Ainsi, on peut également affirmer qu'il faut exécuter la suite de tests de superclasse dans le contexte de n'importe quelle sous-classe.

Voir aussi la réponse à la question Stackoverflow " Puis-je implémenter une série de tests réutilisables pour tester l'implémentation d'une interface? "

avandeursen
la source
14

Illustrons en Java:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Il n'y a pas de problème ici, non? Une voiture est définitivement un moyen de transport, et nous pouvons voir ici qu'elle remplace la méthode startEngine () de sa superclasse.

Ajoutons un autre appareil de transport:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

Tout ne se passe pas comme prévu maintenant! Oui, un vélo est un appareil de transport, cependant, il n'a pas de moteur et, par conséquent, la méthode startEngine () ne peut pas être implémentée.

Ce sont les types de problèmes que conduit à la violation du principe de substitution de Liskov, et ils peuvent le plus souvent être reconnus par une méthode qui ne fait rien, ou même ne peut pas être implémentée.

La solution à ces problèmes est une hiérarchie d'héritage correcte, et dans notre cas, nous résoudrions le problème en différenciant les classes de dispositifs de transport avec et sans moteur. Même si un vélo est un moyen de transport, il n'a pas de moteur. Dans cet exemple, notre définition de dispositif de transport est erronée. Il ne devrait pas avoir de moteur.

Nous pouvons refactoriser notre classe TransportationDevice comme suit:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Nous pouvons maintenant étendre TransportationDevice pour les appareils non motorisés.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

Et étendez TransportationDevice pour les appareils motorisés. Il est plus approprié d'ajouter l'objet Engine.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

Ainsi, notre classe Car devient plus spécialisée, tout en adhérant au principe de substitution Liskov.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

Et notre classe de vélos est également conforme au principe de substitution Liskov.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}
Khaled Qasem
la source
9

Cette formulation du LSP est beaucoup trop forte:

Si pour chaque objet o1 de type S il y a un objet o2 de type T tel que pour tous les programmes P définis en termes de T, le comportement de P reste inchangé lorsque o1 se substitue à o2, alors S est un sous-type de T.

Ce qui signifie essentiellement que S est une autre implémentation complètement encapsulée de la même chose que T. Et je pourrais être audacieux et décider que les performances font partie du comportement de P ...

Donc, fondamentalement, toute utilisation de liaison tardive viole le LSP. C'est tout l'intérêt d'OO pour obtenir un comportement différent quand on substitue un objet d'un genre à un autre!

La formulation citée par wikipedia est meilleure car la propriété dépend du contexte et n'inclut pas nécessairement l'ensemble du comportement du programme.

Damien Pollet
la source
2
Euh, cette formulation est la propre de Barbara Liskov. Barbara Liskov, «Abstraction et hiérarchie des données», Notices SIGPLAN, 23,5 (mai 1988). Ce n'est pas "beaucoup trop fort", c'est "tout à fait exact", et cela n'a pas l'implication que vous pensez qu'elle a. Il est solide, mais a juste la bonne quantité de force.
DrPizza
Ensuite, il y a très peu de sous-types dans la vraie vie :)
Damien Pollet
3
"Le comportement est inchangé" ne signifie pas qu'un sous-type vous donnera exactement les mêmes valeurs de résultat concrètes. Cela signifie que le comportement du sous-type correspond à ce qui est attendu dans le type de base. Exemple: le type de base Shape peut avoir une méthode draw () et stipuler que cette méthode doit rendre la forme. Deux sous-types de forme (par exemple carré et cercle) implémenteraient tous deux la méthode draw () et les résultats seraient différents. Mais tant que le comportement (rendu de la forme) correspondait au comportement spécifié de Shape, alors Square et Circle seraient des sous-types de Shape conformément au LSP.
SteveT
9

Dans une phrase très simple, nous pouvons dire:

La classe enfant ne doit pas violer ses caractéristiques de classe de base. Il doit en être capable. Nous pouvons dire que c'est la même chose que le sous-typage.

Alireza Rahmani Khalili
la source
9

Principe de substitution de Liskov (LSP)

Tout le temps, nous concevons un module de programme et nous créons des hiérarchies de classes. Ensuite, nous étendons certaines classes en créant des classes dérivées.

Nous devons nous assurer que les nouvelles classes dérivées s'étendent simplement sans remplacer la fonctionnalité des anciennes classes. Sinon, les nouvelles classes peuvent produire des effets indésirables lorsqu'elles sont utilisées dans des modules de programme existants.

Le principe de substitution de Liskov stipule que si un module de programme utilise une classe de base, la référence à la classe de base peut être remplacée par une classe dérivée sans affecter la fonctionnalité du module de programme.

Exemple:

Voici l'exemple classique pour lequel le principe de substitution de Liskov est violé. Dans l'exemple, 2 classes sont utilisées: Rectangle et Carré. Supposons que l'objet Rectangle soit utilisé quelque part dans l'application. Nous étendons l'application et ajoutons la classe Square. La classe carrée est renvoyée par un modèle d'usine, basé sur certaines conditions et nous ne savons pas exactement quel type d'objet sera retourné. Mais nous savons que c'est un rectangle. Nous obtenons l'objet rectangle, définissons la largeur à 5 et la hauteur à 10 et obtenons l'aire. Pour un rectangle de largeur 5 et de hauteur 10, la zone doit être 50. Au lieu de cela, le résultat sera 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

Conclusion:

Ce principe n'est qu'une extension du principe Open Close et cela signifie que nous devons nous assurer que les nouvelles classes dérivées étendent les classes de base sans changer leur comportement.

Voir aussi: Open Close Principle

Quelques concepts similaires pour une meilleure structure: convention sur la configuration

GauRang Omar
la source
8

Le principe de substitution de Liskov

  • La méthode substituée ne doit pas rester vide
  • La méthode substituée ne doit pas générer d'erreur
  • Le comportement de la classe de base ou de l'interface ne doit pas être modifié (retravaillé) en raison des comportements de classe dérivés.
Rahamath
la source
7

Un addendum:
je me demande pourquoi personne n'a écrit sur l'invariant, les conditions préalables et les conditions de publication de la classe de base qui doivent être respectées par les classes dérivées. Pour qu'une classe D dérivée soit entièrement soutenable par la classe B de base, la classe D doit respecter certaines conditions:

  • Les variantes internes de la classe de base doivent être préservées par la classe dérivée
  • Les conditions préalables de la classe de base ne doivent pas être renforcées par la classe dérivée
  • Les post-conditions de la classe de base ne doivent pas être affaiblies par la classe dérivée.

Ainsi, le dérivé doit être conscient des trois conditions ci-dessus imposées par la classe de base. Par conséquent, les règles de sous-typage sont prédéterminées. Ce qui signifie que la relation 'EST A' ne doit être respectée que lorsque certaines règles sont respectées par le sous-type. Ces règles, sous forme d'invariants, de précoditions et de postcondition, devraient être décidées par un « contrat de conception » formel .

D'autres discussions à ce sujet sont disponibles sur mon blog: Principe de substitution de Liskov

aknon
la source
6

Le LSP indique en termes simples que les objets de la même superclasse devraient pouvoir être échangés entre eux sans rien casser.

Par exemple, si nous avons une classe Catet une Dogdérivée d'une Animalclasse, toutes les fonctions utilisant la classe Animal devraient pouvoir utiliser Catou Doget se comporter normalement.

johannesMatevosyan
la source
4

Est-ce que la mise en œuvre de ThreeDBoard en termes de tableau de bord serait si utile?

Peut-être voudrez-vous traiter des tranches de ThreeDBoard dans divers plans comme une carte. Dans ce cas, vous souhaiterez peut-être extraire une interface (ou une classe abstraite) pour que Board autorise plusieurs implémentations.

En termes d'interface externe, vous voudrez peut-être prendre en compte une interface de carte pour TwoDBoard et ThreeDBoard (bien qu'aucune des méthodes ci-dessus ne convienne).

Tom Hawtin - sellerie
la source
1
Je pense que l'exemple est simplement pour démontrer que l'héritage de board n'a pas de sens avec dans le contexte de ThreeDBoard et toutes les signatures de méthode sont dénuées de sens avec un axe Z.
NotMyself
4

Un carré est un rectangle dont la largeur est égale à la hauteur. Si le carré définit deux tailles différentes pour la largeur et la hauteur, il viole l'invariant carré. Ceci est contourné en introduisant des effets secondaires. Mais si le rectangle avait un setSize (hauteur, largeur) avec la condition préalable 0 <hauteur et 0 <largeur. La méthode de sous-type dérivée nécessite hauteur == largeur; une condition préalable plus forte (et qui viole lsp). Cela montre que bien que carré soit un rectangle, ce n'est pas un sous-type valide car la condition préalable est renforcée. Le travail autour (en général une mauvaise chose) provoque un effet secondaire et cela affaiblit la condition de poste (qui viole lsp). setWidth sur la base a une condition de post 0 <largeur. Le dérivé l'affaiblit avec hauteur == largeur.

Par conséquent, un carré redimensionnable n'est pas un rectangle redimensionnable.

Wouter
la source
4

Ce principe a été introduit par Barbara Liskov en 1987 et étend le principe ouvert-fermé en se concentrant sur le comportement d'une superclasse et de ses sous-types.

Son importance devient évidente lorsque nous considérons les conséquences de sa violation. Considérez une application qui utilise la classe suivante.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

Imaginez qu'un jour, le client exige la possibilité de manipuler des carrés en plus des rectangles. Puisqu'un carré est un rectangle, la classe carrée doit être dérivée de la classe Rectangle.

public class Square : Rectangle
{
} 

Cependant, ce faisant, nous rencontrerons deux problèmes:

Un carré n'a pas besoin à la fois de variables de hauteur et de largeur héritées du rectangle et cela pourrait créer un gaspillage de mémoire important si nous devons créer des centaines de milliers d'objets carrés. Les propriétés de définition de largeur et de hauteur héritées du rectangle sont inappropriées pour un carré car la largeur et la hauteur d'un carré sont identiques. Afin de définir à la fois la hauteur et la largeur sur la même valeur, nous pouvons créer deux nouvelles propriétés comme suit:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Maintenant, lorsque quelqu'un définira la largeur d'un objet carré, sa hauteur changera en conséquence et vice-versa.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Allons de l'avant et considérons cette autre fonction:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

Si nous transmettons une référence à un objet carré dans cette fonction, nous violerions le LSP car la fonction ne fonctionne pas pour les dérivés de ses arguments. Les propriétés largeur et hauteur ne sont pas polymorphes car elles ne sont pas déclarées virtuelles dans un rectangle (l'objet carré sera corrompu car la hauteur ne sera pas modifiée).

Cependant, en déclarant les propriétés du setter virtuelles, nous ferons face à une autre violation, l'OCP. En fait, la création d'un carré de classe dérivé entraîne des modifications du rectangle de classe de base.

Ivan Porta
la source
3

L'explication la plus claire pour le LSP que j'ai trouvée jusqu'à présent a été "Le principe de substitution de Liskov dit que l'objet d'une classe dérivée devrait être capable de remplacer un objet de la classe de base sans apporter d'erreurs dans le système ou modifier le comportement de la classe de base "d' ici . L'article donne un exemple de code pour violer LSP et le corriger.

Prasa
la source
1
Veuillez fournir des exemples de code sur stackoverflow.
sebenalern
3

Disons que nous utilisons un rectangle dans notre code

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

Dans notre classe de géométrie, nous avons appris qu'un carré est un type spécial de rectangle car sa largeur est la même longueur que sa hauteur. Faisons également un Squarecours sur la base de ces informations:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Si nous remplaçons le Rectanglepar Squaredans notre premier code, il se cassera:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

En effet , l' Squarea une nouvelle condition que nous n'avions pas dans la Rectangleclasse: width == height. Selon LSP, les Rectangleinstances devraient être substituables aux Rectangleinstances de sous-classe. Cela est dû au fait que ces instances réussissent la vérification de type des Rectangleinstances et provoquent donc des erreurs inattendues dans votre code.

C'était un exemple pour la partie "les conditions préalables ne peuvent pas être renforcées dans un sous-type" dans l' article wiki . Donc, pour résumer, la violation de LSP entraînera probablement des erreurs dans votre code à un moment donné.

inf3rno
la source
3

LSP dit que «les objets doivent être remplaçables par leurs sous-types». D'un autre côté, ce principe

Les classes enfants ne doivent jamais casser les définitions de type de la classe parent.

et l'exemple suivant aide à mieux comprendre le LSP.

Sans LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Fixation par LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}
Zahra.HY
la source
2

Je vous encourage à lire l'article: Violating Liskov Substitution Principle (LSP) .

Vous pouvez y trouver une explication sur le principe de substitution de Liskov, des indices généraux vous aidant à deviner si vous l'avez déjà violé et un exemple d'approche qui vous aidera à rendre votre hiérarchie de classes plus sûre.

Ryszard Dżegan
la source
2

LE PRINCIPE DE SUBSTITUTION DE LISKOV (extrait du livre de Mark Seemann) stipule que nous devrions être en mesure de remplacer une implémentation d'une interface par une autre sans rompre ni client ni implémentation.C'est ce principe qui permet de répondre aux exigences qui se produiront à l'avenir, même si nous le pouvons '' t les prévoir aujourd'hui.

Si nous débranchons l'ordinateur du mur (mise en œuvre), ni la prise murale (interface) ni l'ordinateur (client) ne tombe en panne (en fait, s'il s'agit d'un ordinateur portable, il peut même fonctionner sur ses batteries pendant un certain temps) . Avec un logiciel, cependant, un client s'attend souvent à ce qu'un service soit disponible. Si le service a été supprimé, nous obtenons une exception NullReferenceException. Pour faire face à ce type de situation, nous pouvons créer une implémentation d'une interface qui ne fait «rien». Il s'agit d'un modèle de conception connu sous le nom d'objet nul [4], qui correspond à peu près au débranchement de l'ordinateur du mur. Parce que nous utilisons un couplage lâche, nous pouvons remplacer une implémentation réelle par quelque chose qui ne fait rien sans causer de problèmes.

Raghu Reddy Muttana
la source
2

Le principe de substitution de Likov stipule que si un module de programme utilise une classe de base, la référence à la classe de base peut être remplacée par une classe dérivée sans affecter la fonctionnalité du module de programme.

Intention - Les types dérivés doivent être totalement substituables à leurs types de base.

Exemple - Types de retour co-variant en java.

Ishan Aggarwal
la source
1

Voici un extrait de cet article qui clarifie bien les choses:

[..] afin de comprendre certains principes, il est important de savoir quand il a été violé. Voilà ce que je vais faire maintenant.

Que signifie la violation de ce principe? Cela implique qu'un objet ne remplit pas le contrat imposé par une abstraction exprimée avec une interface. En d'autres termes, cela signifie que vous avez mal identifié vos abstractions.

Prenons l'exemple suivant:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

Est-ce une violation du LSP? Oui. C'est parce que le contrat du compte nous dit qu'un compte serait retiré, mais ce n'est pas toujours le cas. Alors, que dois-je faire pour y remédier? Je modifie juste le contrat:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà, maintenant le contrat est satisfait.

Cette violation subtile impose souvent à un client la capacité de faire la différence entre les objets concrets employés. Par exemple, étant donné le premier contrat du compte, il pourrait ressembler à ceci:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

Et, cela viole automatiquement le principe ouvert-fermé [c'est-à-dire pour l'exigence de retrait d'argent. Parce que vous ne savez jamais ce qui se passe si un objet violant le contrat n'a pas assez d'argent. Il ne retourne probablement rien, probablement une exception sera levée. Vous devez donc vérifier s'il ne hasEnoughMoney()fait pas partie d'une interface. Cette vérification forcée dépendante de la classe béton est donc une violation OCP].

Ce point aborde également une idée fausse que je rencontre assez souvent au sujet de la violation du LSP. Il dit que «si le comportement d'un parent change chez un enfant, alors, il viole le LSP». Cependant, ce n'est pas le cas - tant qu'un enfant ne viole pas le contrat de ses parents.

Vadim Samokhin
la source