Quel est un exemple du principe de substitution de Liskov?

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

    Un bon exemple illustrant le LSP (donné par 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 vraiment dans le 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 inheritance. Cependant, si dans le code que vous avez créé Square provient de Rectangle , alors un Square devrait être utilisable partout où vous attendez un Rectangle . Cela crée un comportement étrange.

    Imaginez que vous SetHeight méthodes SetWidth et SetHeight sur votre classe de base Rectangle . cela semble parfaitement logique. Cependant, si votre référence Rectangle pointe sur un Square , SetWidth et SetHeight pas de sens, car la définition de l’un changerait l’autre pour la faire correspondre. Dans ce cas, Square échoue au test de substitution de Liskov avec Rectangle et l’abstraction du fait d’avoir Square hérité de Rectangle est mauvaise.

    Vous devriez vérifier les autres affiches de motivation SOLID Principles .

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

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

    En son cœur, le LSP concerne les interfaces et les contrats, ainsi que la manière de décider quand prolonger une classe par rapport à une autre stratégie telle que la composition pour atteindre votre objective.

    Le moyen le plus efficace pour illustrer ce point a été présenté dans Head First OOA & D. Ils présentent un scénario où vous êtes un développeur sur un projet visant à créer 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 parameters pour localiser la position de la mosaïque dans le tableau bidimensionnel de Tiles . Cela permettra à un développeur de jeux de gérer des unités dans le tableau au cours de la partie.

    Le livre continue à modifier les exigences pour dire que le travail de la trame de jeu doit également prendre en charge les tableaux de jeu 3D pour accueillir les jeux en vol. Donc, une classe ThreeDBoard est introduite qui étend Board .

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

    Là où ça se casse, c’est quand on regarde tous les autres membres hérités du Board . Les méthodes pour AddUnit , GetTile , GetUnits , etc. prennent toutes les deux les parameters X et Y dans la classe Board , mais ThreeDBoard besoin d’un paramètre Z.

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

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

    Cela nous permet d’utiliser de bons principes orientés object tels que l’encapsulation et la réutilisation et ne viole pas le LSP.

    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, même si l’interface correspond. 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 getters et setters fonctionnent, un 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 implémentation correcte de Square , par conséquent il ne s’agit pas d’un substitut valide de Rectangle .

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

    Certaines 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 des informations de type à l’exécution C ++ (RTTI) pour sélectionner une fonction en fonction du type d’object. c’est à dire:

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

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

    Carré et rectangle, une violation plus subtile.

    Cependant, il existe d’autres moyens beaucoup plus subtils de violer le LSP. Considérons une application qui utilise la classe Rectangle 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 demandent la possibilité de manipuler des carrés en plus des rectangles. […]

    Clairement, un carré est un rectangle pour toutes les intentions et tous les buts normaux. Étant donné que la relation ISA est valide, il est logique de modéliser la classe Square comme étant dérivée de Rectangle . […]

    Square héritera des fonctions SetWidth et SetHeight . Ces fonctions sont totalement inappropriées pour un Square , car 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 remplacer SetWidth et 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 object Square dans cette fonction, l’object Square sera corrompu car la hauteur ne sera pas modifiée. Ceci est une violation claire du LSP. La fonction ne fonctionne pas pour les dérivés de ses arguments.

    […]

    Le 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 que S hérite, dérive ou est un sous-type T ) .

    Par exemple, cela se produit lorsqu’une fonction avec un paramètre d’entrée de type T est appelée (c’est-à-dire appelée) avec une valeur d’argument de type S Ou, lorsqu’un identificateur de type T est affecté à une valeur de type S

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

    LSP exige 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 parameters de rectangle immuables s’attendent à ce que les dimensions soient modifiées indépendamment, mais les parameters carrés 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 exige que chaque méthode du sous-type S ait un ou des parameters d’entrée contravariants et une sortie covariante.

    Contravariant signifie que la variance est contraire à la direction de l’inheritance, c’est-à-dire que 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 sur-type de 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 le même sens de l’inheritance, c’est-à-dire que 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 supertype T

    En effet, si l’appelant pense avoir un type T , pense appeler une méthode de T , il fournit alors le ou les arguments de type Ti et affecte la sortie au type To . Lorsqu’il appelle la méthode correspondante de S , chaque argument d’entrée Ti est affecté à un paramètre d’entrée Si et la sortie So est affectée au type To . Donc, si Si n’était pas contravariant à Ti , alors un sous-type Xi – qui ne serait pas un sous-type de Si pourrait être atsortingbué à Ti .

    De plus, pour les langages (par exemple Scala ou Ceylon) qui ont des annotations de variance de site de définition sur des parameters de polymorphism de type (génériques), la co-direction ou la contre-direction de l’annotation de variance pour chaque paramètre de type T direction respectivement à chaque paramètre d’entrée ou sortie (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, le sens de variance requirejs est inversé. Cette règle est appliquée de manière récursive.


    Le sous-typage est approprié lorsque 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.

    Typestate (voir page 3) déclare et applique les invariants d’état orthogonaux au type. Les invariants peuvent également être appliqués en convertissant 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 () non disponible dans Fichier. Une API tic-tac-toe peut être un autre exemple d’utilisation de la saisie pour imposer des invariants au moment de la compilation. Le système de type peut même être Turing-complet, par exemple 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 de la nécessité pour la sémantique d’ abstraire sur l’extension , j’imagine que l’utilisation de la typage pour modéliser les invariants, c’est-à-dire la sémantique dénotationnelle unifiée d’ordre supérieur, est supérieure à la typestate. “Extension” signifie la composition sans limite 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é, avoir deux modèles mutuellement dépendants (par exemple, types et typestate) pour exprimer la sémantique partagée, qui ne peuvent être unifiés les uns avec les autres pour une composition extensible . Par exemple, l’ extension similaire à Expression Problem a été unifiée dans les domaines de sous-typage, de surcharge de fonctions et de typage paramésortingque.

    Ma position théorique est que pour que la connaissance existe (voir la section «La centralisation est aveugle et inapte»), il n’y aura jamais de modèle général capable d’imposer une couverture à 100% de tous les invariants possibles dans un langage informatique complet. 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 mise en pause existe, c’est-à-dire qu’il est indécis que tous les programmes possibles dans un langage de programmation Turing-complet se terminent. Il peut être prouvé qu’un programme spécifique se termine (celui 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 terminées (par exemple, via la saisie dépendante). Comme l’exigence fondamentale de la complétude de Turing est la récursivité 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:

    • 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. Ainsi, les ensembles ne peuvent pas être étendus ou ce sont des récursions illimitées. Par exemple, l’ensemble de tout ce qui n’est pas une théière se comprend, ce qui inclut 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.-à-d. Autorise tous les types non spécifiés) et n’autorise pas les extensions non liées. 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 la Subdivision de Liskov : généralement, le fait de savoir si un ensemble est le sous-ensemble d’un autre est un problème indécidable, c’est-à-dire que l’inheritance est généralement indécidable.
    • Linsky Referencing : le calcul de quelque chose, quand il est décrit ou perçu, c’est-à-dire indécidable, c’est-à-dire que la perception (la réalité) n’a pas de sharepoint référence absolu.
    • Le théorème de Coase : il n’y a pas de sharepoint référence externe, donc toute barrière aux possibilités externes illimitées échouera.
    • Deuxième loi de la thermodynamic : l’univers entier (un système fermé, c’est-à-dire tout) évolue vers un désordre maximal, c’est-à-dire des possibilités indépendantes maximales.

    La substituabilité est un principe de la programmation orientée object indiquant que, dans un programme informatique, si S est un sous-type de T, alors les objects de type T peuvent être remplacés par des objects 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 oiseau, mais qu’en est-il de cela:

     public class Ossortingch extends Bird{} 

    L’authive est un oiseau, mais elle ne peut pas voler, la classe d’authive est un sous-type de la classe Bird, mais elle ne peut pas utiliser la méthode de la mouche, ce qui signifie que nous brisons le principe du LSP.

    Bon exemple

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

    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 le LSP si chaque fois que vous appelez Foo sur un object dérivé, il donne exactement les mêmes résultats que lorsque vous appelez Foo sur un object Base, à condition que arg soit identique.

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

    Quand j’ai lu pour la première fois à propos de LSP, j’ai supposé que cela signifiait au sens ssortingct, en le assimilant essentiellement à l’implémentation d’interface et à la diffusion de type sécurisé. Ce qui signifierait que le LSP est assuré ou non par la langue elle-même. Par exemple, dans ce sens ssortingct, ThreeDBoard est certainement substituable à Board, en ce qui concerne le compilateur.

    Après avoir lu plus sur le concept bien que 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’object 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 peut également être testée en sondant le comportement réel des objects. Autrement dit, examiner l’impact des arguments d’état et de méthode d’un object sur les résultats des appels de méthode ou les types d’exceptions lancés à partir de l’object.

    Pour en revenir à l’exemple, les méthodes du conseil peuvent théoriquement fonctionner sur ThreeDBoard. En 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 la fonctionnalité que ThreeDBoard est destiné à append.

    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’inheritance.

    Curieusement, personne n’a posté le papier original décrivant lsp. Ce n’est pas une lecture facile comme celle de Robert Martin, mais cela en vaut la peine.

    Un exemple important de l’ utilisation du LSP concerne les tests logiciels .

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

    Pour tester complètement la sous-classe A, je dois probablement append 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 réaliser cela est de construire ce que McGregor appelle une “hiérarchie parallèle pour les tests”: ma classe ATest héritera de BTest . Une forme d’injection est alors nécessaire pour garantir que le scénario de test fonctionne avec des objects de type A plutôt que de type B (un modèle de méthode de modèle simple suffira).

    Notez que la réutilisation de la suite de super-tests pour toutes les implémentations de sous-classes est en fait un moyen de tester que ces implémentations de sous-classes sont compatibles avec LSP. Ainsi, on peut également faire valoir que l’on devrait exécuter la suite de tests de la 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? ”

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

    • Si vous violez l’un des éléments suivants -> vous violez Liskov.
    • Si vous ne violez aucun -> ne peut rien conclure.

    Liste de vérification:

    • 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 ont uniquement été autorisées à émettre des exceptions de type ArgumentNullException ou des exceptions dérivées d’ArgumentNullException. Lancer IndexOutOfRangeException est une violation de Liskov.
    • Les conditions préalables ne peuvent pas être renforcées : Supposons que votre classe de base fonctionne avec un membre int. Maintenant, votre sous-type nécessite que l’int soit positif. Ceci est renforcé des conditions préalables, et maintenant tout code qui fonctionnait parfaitement bien avant avec des ints négatifs est cassé.
    • Les post-conditions ne peuvent pas être affaiblies : Supposons que votre classe de base nécessite que toutes les connexions à la firebase database soient fermées avant le retour de la méthode. Dans votre sous-classe, vous avez annulé cette méthode et la connexion laissée 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 la plus pénible à remplir. Les invariants sont 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 vous assurer que lorsque vous remplacez une méthode, tout élément non modifiable doit restr inchangé après l’exécution de la méthode remplacée. La meilleure chose à laquelle je peux penser est d’appliquer ces contraintes invariantes dans la classe de base, mais ce ne serait pas facile.
    • Contrainte de l’historique : Lorsque vous modifiez une méthode, vous n’êtes pas autorisé à modifier une propriété non modifiable dans la classe de base. Jetez un coup d’oeil à ces codes et vous pouvez voir que Name est défini pour être non modifiable (ensemble privé) mais SubType introduit une nouvelle méthode qui permet de le modifier (par reflection):

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

    Il y a 2 autres items: Contravariance des arguments de la méthode et Covariance des types de retour . Mais ce n’est pas possible en C # (je suis un développeur C #) donc je ne m’en soucie pas.

    Référence:

    Je suppose que tout le monde a couvert ce que LSP est techniquement: vous voulez fondamentalement être en mesure d’abstraire des détails de sous-type et d’utiliser des supertypes en toute sécurité.

    Donc, Liskov a 3 règles sous-jacentes:

    1. Règle de signature: Il devrait 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 y a une petite règle à propos de jeter moins d’exceptions et d’être au moins aussi accessible que les méthodes de supertypes.

    2. Méthodes Règle: L’implémentation de ces opérations est sémantiquement saine.

      • 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: Ils devraient produire un sous-ensemble du résultat des méthodes de supertypes produites.
    3. Règle de propriétés: Cela va au-delà des appels de fonction individuels.

      • Invariants : Things that are always true must remain true. Par exemple. a Set’s size is never negative.
      • Evolutionary Properties : Usually something to do with immutability or the kind of states the object can be in. Or maybe the object only grows and never shrinks so the subtype methods shouldn’t make it.

    All these properties need to be preserved and the extra subtype functionality shouldn’t violate supertype properties.

    If these three things are taken care of , you have abstracted away from the underlying stuff and you are writing loosely coupled code.

    Source: Program Development in Java – Barbara Liskov

    This formulation of the LSP is way too strong:

    If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.

    Which basically means that S is another, completely encapsulated implementation of the exact same thing as T. And I could be bold and decide that performance is part of the behavior of P…

    So, basically, any use of late-binding violates the LSP. It’s the whole point of OO to to obtain a different behavior when we substitute an object of one kind for one of another kind!

    The formulation cited by wikipedia is better since the property depends on the context and does not necessarily include the whole behavior of the program.

    Some addendum:
    I wonder why didn’t anybody write about the Invariant , preconditions and post conditions of the base class that must be obeyed by the derived classes. For a derived class D to be completely sustitutable by the Base class B, class D must obey certain conditions:

    • In-variants of base class must be preserved by the derived class
    • Pre-conditions of the base class must not be strengthened by the derived class
    • Post-conditions of the base class must not be weakened by the derived class.

    So the derived must be aware of the above three conditions imposed by the base class. Hence, the rules of subtyping are pre-decided. Which means, ‘IS A’ relationship shall be obeyed only when certain rules are obeyed by the subtype. These rules, in the form of invariants, precoditions and postcondition, should be decided by a formal ‘ design contract ‘.

    Further discussions on this available at my blog: Liskov Substitution principle

    Long story short, let’s leave rectangles rectangles and squares squares, practical example when extending a parent class, you have to either PRESERVE the exact parent API or to EXTEND IT.

    Let’s say you have a base ItemsRepository.

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

    And a sub class extending it:

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

    Then you could have a Client working with the Base ItemsRepository API and relying on it.

     /** * 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(); } } 

    The LSP is broken when substituting parent class with a sub class breaks the API’s contract .

     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 :( } } 

    In a very simple sentence, we can say:

    The child class must not violate its base class characteristics. It must be capable with it. We can say it’s same as subtyping.

    A square is a rectangle where the width equals the height. If the square sets two different sizes for the width and height it violates the square invariant. This is worked around by introducing side effects. But if the rectangle had a setSize(height, width) with precondition 0 < height and 0 < width. The derived subtype method requires height == width; a stronger precondition (and that violates lsp). This shows that though square is a rectangle it is not a valid subtype because the precondition is strengthened. The work around (in general a bad thing) cause a side effect and this weakens the post condition (which violates lsp). setWidth on the base has post condition 0 < width. The derived weakens it with height == width.

    Therefore a resizable square is not a resizable rectangle.

    I see rectangles and squares in every answer, and how to violate the LSP.

    I’d like to show how the LSP can be conformed to with a real world example :

      

    This design conforms to the LSP because the behavior remains unchanged regardless of the implementation we choose to use.

    And yes, you can violate LSP in this configuration doing one simple change like so :

      $result]; // This violates LSP ! } } 

    Now the subtypes cannot be used the same way since they don't produce the same result anymore.

    Would implementing ThreeDBoard in terms of an array of Board be that useful?

    Perhaps you may want to treat slices of ThreeDBoard in various planes as a Board. In that case you may want to abstract out an interface (or abstract class) for Board to allow for multiple implementations.

    In terms of external interface, you might want to factor out a Board interface for both TwoDBoard and ThreeDBoard (although none of the above methods fit).

    I encourage you to read the article: Violating Liskov Substitution Principle (LSP) .

    You can find there an explanation what is the Liskov Substitution Principle, general clues helping you to guess if you have already violated it and an example of approach that will help you to make your class hierarchy be more safe.

    The clearest explanation for LSP I found so far has been “The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class” from here . The article gives code example for violating LSP and fixing it.

    LISKOV SUBSTITUTION PRINCIPLE (From Mark Seemann book) states that we should be able to replace one implementation of an interface with another without breaking either client or implementation.It’s this principle that enables to address requirements that occur in the future, even if we can’t foresee them today.

    If we unplug the computer from the wall (Implementation), neither the wall outlet (Interface) nor the computer (Client) breaks down (in fact, if it’s a laptop computer, it can even run on its batteries for a period of time). With software, however, a client often expects a service to be available. If the service was removed, we get a NullReferenceException. To deal with this type of situation, we can create an implementation of an interface that does “nothing.” This is a design pattern known as Null Object,[4] and it corresponds roughly to unplugging the computer from the wall. Because we’re using loose coupling, we can replace a real implementation with something that does nothing without causing trouble.

    Let’s say we use a rectangle in our code

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

    In our geometry class we learned that a square is a special type of rectangle because its width is the same length as its height. Let’s make a Square class as well based on this info:

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

    If we replace the Rectangle with Square in our first code, then it will break:

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

    This is because the Square has a new precondition we did not have in the Rectangle class: width == height . According to LSP the Rectangle instances should be substitutable with Rectangle subclass instances. This is because these instances pass the type check for Rectangle instances and so they will cause unexpected errors in your code.

    This was an example for the “preconditions cannot be strengthened in a subtype” part in the wiki article . So to sum up, violating LSP will probably cause errors in your code at some point.

    Liskov’s Substitution Principle(LSP)

    All the time we design a program module and we create some class hierarchies. Then we extend some classes creating some derived classes.

    We must make sure that the new derived classes just extend without replacing the functionality of old classes. Otherwise, the new classes can produce undesired effects when they are used in existing program modules.

    Liskov’s Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.

    Exemple:

    Below is the classic example for which the Liskov’s Substitution Principle is violated. In the example, 2 classes are used: Rectangle and Square. Let’s assume that the Rectangle object is used somewhere in the application. We extend the application and add the Square class. The square class is returned by a factory pattern, based on some conditions and we don’t know the exact what type of object will be returned. But we know it’s a Rectangle. We get the rectangle object, set the width to 5 and height to 10 and get the area. For a rectangle with width 5 and height 10, the area should be 50. Instead, the result will be 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(Ssortingng 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:

    This principle is just an extension of the Open Close Principle and it means that we must make sure that new derived classes are extending the base classes without changing their behavior.

    See also: Open Close Principle

    Some similar concepts for better structure: Convention over configuration

    Likov’s Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.

    Intent – Derived types must be completely substitute able for their base types.

    Example – Co-variant return types in java.

    Here is an excerpt from this post that clarifies things nicely:

    [..] in order to comprehend some principles, it’s important to realize when it’s been violated. This is what I will do now.

    What does the violation of this principle mean? It implies that an object doesn’t fulfill the contract imposed by an abstraction expressed with an interface. In other words, it means that you identified your abstractions wrong.

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

    Is this a violation of LSP? Oui. This is because the account’s contract tells us that an account would be withdrawn, but this is not always the case. So, what should I do in order to fix it? I just modify the contract:

     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à, now the contract is satisfied.

    This subtle violation often imposes a client with the ability to tell the difference between concrete objects employed. For example, given the first Account’s contract, it could look like the following:

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

    And, this automatically violates the open-closed principle [that is, for money withdrawal requirement. Because you never know what happens if an object violating the contract doesn’t have enough money. Probably it just returns nothing, probably an exception will be thrown. So you have to check if it hasEnoughMoney() — which is not part of an interface. So this forced concrete-class-dependent check is an OCP violation].

    This point also addresses a misconception that I encounter quite often about LSP violation. It says the “if a parent’s behavior changed in a child, then, it violates LSP.” However, it doesn’t — as long as a child doesn’t violate its parent’s contract.