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?
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?
Réponses:
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
Square
est aRectangle
. 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 avezSquare
dérivéRectangle
, aSquare
devrait être utilisable partout où vous vous attendez aRectangle
. Cela crée un comportement étrange.Imaginez que vous aviez
SetWidth
et lesSetHeight
méthodes de votreRectangle
classe de base; cela semble parfaitement logique. Cependant, si votreRectangle
référence pointait vers aSquare
, alorsSetWidth
etSetHeight
n'a pas de sens car le réglage de l'un changerait l'autre pour qu'il corresponde. Dans ce cas,Square
le test de substitution Liskov échoueRectangle
et l'abstraction d'avoirSquare
héritéRectangle
est mauvaise.Vous devriez vérifier les autres affiches de motivation inestimables des principes SOLID .
la source
Square.setWidth(int width)
était implémenté comme cecithis.width = width; this.height = width;
:? Dans ce cas, il est garanti que la largeur est égale à la hauteur.Le principe de substitution de Liskov (LSP, lsp) est un concept de programmation orientée objet qui stipule:
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:
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
ThreeDBoard
classe est introduite qui s'étendBoard
.À première vue, cela semble être une bonne décision.
Board
fournit à la fois leHeight
etWidth
propriétés etThreeDBoard
fournit l'axe Z.Où il tombe en panne, c'est quand vous regardez tous les autres membres hérités
Board
. Les méthodes pourAddUnit
,GetTile
,GetUnits
et ainsi de suite, toutes les deux paramètres X et Y dans laBoard
classe mais aThreeDBoard
besoin 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
Board
classe et les méthodes héritées de laBoard
classe perdent leur signification. Une unité de code tentant d'utiliser laThreeDBoard
classe comme classe de baseBoard
serait très malchanceuse.Nous devrions peut-être trouver une autre approche. Au lieu de s'étendre
Board
,ThreeDBoard
devrait être composé d'Board
objets. UnBoard
objet par unité de l'axe Z.Cela nous permet d'utiliser de bons principes orientés objet comme l'encapsulation et la réutilisation et ne viole pas le LSP.
la source
faisons un exemple simple en Java:
Mauvais exemple
Le canard peut voler à cause de c'est un oiseau, mais qu'en est-il:
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
la source
Bird bird
. Vous devez lancer l'objet sur FlyingBirds pour utiliser fly, ce qui n'est pas bien non?Bird bird
, cela signifie qu'il ne peut pas l'utiliserfly()
. C'est ça. Passer unDuck
ne change rien à ce fait. Si le client aFlyingBirds bird
, alors même s'il est adopté,Duck
il devrait toujours fonctionner de la même manière.LSP concerne les invariants.
L'exemple classique est donné par la déclaration de pseudo-code suivante (implémentations omises):
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,
Rectangle
devrait satisfaire l'invariant suivant:Cependant, cet invariant doit être violé par une mise en œuvre correcte de
Square
, il n'est donc pas un substitut valide deRectangle
.la source
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é):
la source
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.LSP est nécessaire lorsque certains codes pensent appeler les méthodes d'un type
T
et peuvent, sans le savoir, appeler les méthodes d'un typeS
, oùS extends T
(c'est-à-direS
hérite, dérive ou est un sous-type du supertypeT
).Par exemple, cela se produit lorsqu'une fonction avec un paramètre d'entrée de type
T
est appelée (c'est-à-dire invoquée) avec une valeur d'argument de typeS
. Ou, lorsqu'un identifiant de typeT
se voit attribuer une valeur de typeS
.LSP nécessite que les attentes (c'est-à-dire les invariants) pour les méthodes de type
T
(par exempleRectangle
) ne soient pas violées lorsque les méthodes de typeS
(par exempleSquare
) sont appelées à la place.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.
LSP requiert que chaque méthode du sous-type
S
ait 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
Si
de chaque paramètre d'entrée de chaque méthode du sous - typeS
, doit être le même ou un supertype du typeTi
du paramètre d'entrée correspondant de la méthode correspondante du supertypeT
.La covariance signifie que la variance va dans le même sens que l'héritage, c'est-à-dire que le type
So
de la sortie de chaque méthode du sous-typeS
doit être le même ou un sous -typeTo
du type de la sortie correspondante de la méthode correspondante du supertypeT
.En effet, si l'appelant pense qu'il a un type
T
, pense qu'il appelle une méthode deT
, il fournit alors des arguments de typeTi
et affecte la sortie au typeTo
. Lorsqu'il appelle réellement la méthode correspondante deS
, chaqueTi
argument d'entrée est affecté à unSi
paramètre d'entrée et laSo
sortie est affectée au typeTo
. Ainsi, s'ilsSi
n'étaient pas contravariantsTi
, alors un sous-typeXi
- qui ne serait pas un sous-type de -Si
pourrait ê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
T
doit être opposée ou la même direction respectivement à chaque paramètre d'entrée ou de sortie (de chaque méthode deT
) 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:
la source
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:
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:
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.
la source
Database::selectQuery
pour 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.Il y a une liste de contrôle pour déterminer si vous violez ou non Liskov.
Liste de contrôle:
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):
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:
la source
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
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.
la source
2 + "2"
). Peut-être confondez-vous «fortement tapé» avec «tapé statiquement»?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 .
Et une sous-classe qui l'étend:
Ensuite, un client pourrait travailler avec l'API Base ItemsRepository et s'y fier.
Le LSP est rompu lorsque le remplacement de la classe parent par une sous-classe rompt le contrat de l'API .
Vous pouvez en savoir plus sur l'écriture de logiciels maintenables dans mon cours: https://www.udemy.com/enterprise-php/
la source
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.
la source
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:
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.
Méthodes Règle: La mise en œuvre de ces opérations est sémantiquement saine.
Règle des propriétés: cela va au-delà des appels de fonction individuels.
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
la source
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
ATest
classe héritera deBTest
. 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? "
la source
Illustrons en Java:
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:
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.
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:
Nous pouvons maintenant étendre TransportationDevice pour les appareils non motorisés.
Et étendez TransportationDevice pour les appareils motorisés. Il est plus approprié d'ajouter l'objet Engine.
Ainsi, notre classe Car devient plus spécialisée, tout en adhérant au principe de substitution Liskov.
Et notre classe de vélos est également conforme au principe de substitution Liskov.
la source
Cette formulation du LSP est beaucoup trop forte:
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.
la source
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.
la source
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
Voir aussi: Open Close Principle
Quelques concepts similaires pour une meilleure structure: convention sur la configuration
la source
Le principe de substitution de Liskov
la source
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:
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
la source
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
Cat
et uneDog
dérivée d'uneAnimal
classe, toutes les fonctions utilisant la classe Animal devraient pouvoir utiliserCat
ouDog
et se comporter normalement.la source
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).
la source
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.
la source
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.
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.
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:
Maintenant, lorsque quelqu'un définira la largeur d'un objet carré, sa hauteur changera en conséquence et vice-versa.
Allons de l'avant et considérons cette autre fonction:
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.
la source
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.
la source
Disons que nous utilisons un rectangle dans notre code
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
Square
cours sur la base de ces informations:Si nous remplaçons le
Rectangle
parSquare
dans notre premier code, il se cassera:En effet , l'
Square
a une nouvelle condition que nous n'avions pas dans laRectangle
classe:width == height
. Selon LSP, lesRectangle
instances devraient être substituables auxRectangle
instances de sous-classe. Cela est dû au fait que ces instances réussissent la vérification de type desRectangle
instances 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é.
la source
LSP dit que «les objets doivent être remplaçables par leurs sous-types». D'un autre côté, ce principe
et l'exemple suivant aide à mieux comprendre le LSP.
Sans LSP:
Fixation par LSP:
la source
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.
la source
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.
la source
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.
la source
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:
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:
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:
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.
la source