Quel est l'exemple du principe de substitution de Liskov?
j'ai entendu dire que le principe de substitution de Liskov (LSP) est un principe fondamental de la conception orientée objet. Quel est-il et quels en sont quelques exemples de son utilisation?
26 réponses
un excellent exemple illustrant LSP (donné par L'oncle Bob dans un podcast que j'ai entendu récemment) était comment parfois quelque chose qui sonne bien dans le langage naturel ne fonctionne pas tout à fait dans le code.
En mathématiques, un Square
est un Rectangle
. En effet c'est une spécialisation d'un rectangle. La "est un" vous donne envie de ce modèle avec l'héritage. Cependant si dans le code que vous avez fait Square
dériver de Rectangle
, alors un Square
devrait être utilisable n'importe où vous attendez-vous à un Rectangle
. Cela rend un comportement étrange.
Imaginez que vous aviez SetWidth
et SetHeight
méthodes sur votre classe de base Rectangle
; cela semble parfaitement logique. Cependant, si votre référence Rectangle
pointait vers une référence Square
, alors SetWidth
et SetHeight
n'a pas de sens parce que définir l'un changerait l'autre pour l'apparier. Dans ce cas, Square
ne satisfait pas au Test de substitution de Liskov avec Rectangle
et l'abstraction de avoir Square
hériter de Rectangle
est un mauvais.
Y'all devrait vérifier les autres inestimable de SOLIDES Principes de Motivation Affiches .
le principe de substitution de Liskov (LSP, LSP ) est un concept de Programmation Orientée Objet qui stipule:
fonctions qui utilisent des pointeurs ou les références aux classes de base doivent être capable d'utiliser des objets de classes dérivées sans le savoir.
au cœur de LSP est sur les interfaces et les contrats ainsi que la façon de décider quand étendre une classe vs. utiliser une autre stratégie telle que composition pour atteindre votre objectif.
le moyen le plus efficace que j'ai vu pour illustrer ce point était dans tête première OOA&d . Ils présentent un scénario où vous êtes un développeur sur un projet de construire un cadre pour les jeux de stratégie.
ils présentent une classe qui représente une planche qui ressemble à ceci:
toutes les méthodes prennent X et Y coordonnées comme paramètres pour localiser la position de la tuile dans le tableau bidimensionnel de Tiles
. Cela permettra à un développeur de jeux de gérer des unités dans le plateau au cours du jeu.
le livre continue à changer les exigences pour dire que le travail de cadre de jeu doit également soutenir des planches de jeu 3D pour accommoder les jeux qui ont vol. Ainsi, une classe ThreeDBoard
est introduite qui étend Board
.
à première vue, on dirait que une bonne décision. Board
fournit à la fois les propriétés Height
et Width
et ThreeDBoard
fournit l'axe Z.
Où il se décompose, c'est quand vous regardez tous les autres membres hérités de Board
. Les méthodes AddUnit
, GetTile
, GetUnits
et ainsi de suite, tous prennent les paramètres X et Y dans la classe Board
mais le ThreeDBoard
nécessite aussi un paramètre Z.
donc vous devez mettre en œuvre ces méthodes encore 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 sens. Une unité de code essayant d'utiliser la classe ThreeDBoard
comme classe de base Board
serait très malchanceuse.
peut-être que nous devrions trouver une autre approche. Au lieu d'étendre Board
, ThreeDBoard
devrait être composé d'objets Board
. Un objet Board
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 LSP.
LSP concerne les invariants.
l'exemple classique est donné par la déclaration suivante du pseudo-code (implémentations omises):
class Rectangle {
int getHeight()
void setHeight(int value)
int getWidth()
void setWidth(int value)
}
class Square : Rectangle { }
maintenant nous avons un problème bien que l'interface corresponde. La raison en est que nous avons violé les invariants découlant 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 mise en œuvre correcte de Square
, donc il n'est pas un substitut valide de Rectangle
.
Robert Martin a un excellent papier sur le Principe de Substitution de Liskov . Il traite des façons subtiles et moins subtiles de violer le principe.
certaines parties pertinentes du document (à noter 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 de c++ Information sur le type D'exécution (RTTI) pour sélectionner une fonction basée sur le type d'un objet. c'est à dire:
void DrawShape(const Shape& s) { if (typeid(s) == typeid(Square)) DrawSquare(static_cast<Square&>(s)); else if (typeid(s) == typeid(Circle)) DrawCircle(static_cast<Circle&>(s)); }
il est clair que la fonction
DrawShape
est mal formée. Il doit savoir à propos de chaque dérivé possible de la classeShape
, et il doit être changé chaque fois que de nouveaux dérivés deShape
sont créés. En effet, beaucoup considèrent la structure de cette fonction comme un anathème pour un Design orienté objet.carré et Rectangle, une Violation plus subtile.
cependant, il y a d'autres façons, beaucoup plus subtiles, de violer la LSP. Envisager 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 exigent la capacité de manipuler places en plus des rectangles. [...]
de toute évidence, un carré est un rectangle à toutes fins et intentions normales. Depuis la relation ISA tient, il est logique de modéliser le
Square
classe dérivée deRectangle
. [...]
Square
héritera des fonctionsSetWidth
etSetHeight
. Ils les fonctions sont tout à fait inappropriées pour unSquare
, puisque la largeur et la hauteur d'un carré sont identiques. Cela devrait être un indice révélateur qu'il y a un problème avec la conception. Cependant, il existe un moyen de de contourner le problème. Nous pourrions annulerSetWidth
etSetHeight
[...]mais considérer la fonction suivante:
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
si nous passons une référence à un objet
Square
dans cette fonction, leSquare
objet sera corrompu parce que la hauteur ne sera pas changé. Il s'agit d'une violation évidente de la LSP. La fonction ne fonctionne pas pour les dérivés de ses arguments.[...]
LSP est nécessaire lorsqu'un code pense qu'il appelle les méthodes d'un type T
, et peut sans le savoir appeler les méthodes d'un type S
, où S extends T
(i.e. S
hérite, dérive ou est un sous-type du supertype T
).
par exemple, cela se produit lorsqu'une fonction avec un paramètre d'entrée de type T
, est appelée (i.e. invoquée) avec une valeur d'argument de type S
. Ou, lorsqu'un identificateur le type T
se voit attribuer 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
), qui ne peut être violé lorsque les méthodes de type S
(par exemple Square
) sont appelés à la place.
val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation
même un type avec champs immuables a encore des invariants, par exemple le Rectangle immuable les régleurs s'attendent à ce que les dimensions soient modifiées indépendamment, mais les régleurs 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 de la sous-type S
doit avoir contravariant paramètre d'entrée(s) et un covariant de sortie.
Contravariant signifie que la variance est contraire à la direction de l'héritage, c'est à dire le type Si
, de chaque paramètre d'entrée de chaque méthode d' le sous-type S
, doit être le même ou un supertype du type Ti
du paramètre d'entrée correspondant de la méthode correspondante du supertype T
.
Covariance signifie que la variance est dans la même direction de l'hérédité, c.-à-d. le type So
, de la sortie de chaque méthode du sous-type S
, doit être le même ou un sous-type du type To
de la sortie correspondante de la méthode correspondante du supertype T
.
c'est parce que si l'appelant pense qu'il a un type T
, pense qu'il appelle une méthode de T
, alors il fournit argument(S) de type Ti
et assigne la sortie au type To
. Quand il appelle réellement la méthode correspondante de S
, alors chaque argument d'entrée Ti
est assigné à un paramètre d'entrée Si
, et le La sortie So
est affectée au type To
. Ainsi, si Si
n'étaient pas contravariant W. R. T. à Ti
, un sous-type Xi
- qui ne serait pas un sous-type de Si
- pourrait être affecté à Ti
.
de plus, pour les langues (p. ex. Scala ou Ceylan) qui ont des annotations de variance de définition-site sur les paramètres de polymorphisme de type (c.-à-d. génériques), la co - ou la contredirection de l'annotation de variance pour chaque type paramètre du type T
doit être opposé ou même direction respectivement à chaque paramètre d'entrée ou sortie (de chaque méthode de T
) qui a le type du paramètre de type.
de plus, pour chaque paramètre d'entrée ou extrant ayant un type de fonction, la direction de la variance requise est inversée. Cette règle est appliquée de façon récursive.
le sous-typage est approprié où les invariants peuvent être énumérés.
Il ya beaucoup de recherche en cours sur la façon de modéliser les invariants, de sorte qu'ils sont appliqués par le compilateur.
Typestate (voir page 3) déclare et applique l'état invariants orthogonale de type. Alternativement, les invariants peuvent être appliqués par convertissant les affirmations en types . Par exemple, affirmer qu'un fichier est ouvert avant la fermeture c', puis Fichier.open() peut retourner un type OpenFile, qui contient une méthode close () qui n'est pas disponible dans File. Un Tic-tac-toe API peut être un autre exemple d'utilisation du typage pour imposer des invariants au moment de la compilation. Le système de type peut même être complet, par exemple Scala . Les langages et les provers de théorème de type dépendant formalisent les modèles de typage d'ordre supérieur.
en raison de la nécessité de la sémantique pour résumé par extension , j'attends que le recours à taper pour modèle invariants, c'est à dire unifiée d'ordre supérieur denotational sémantique, est supérieure à la Typestate. "Extension", la composition non borné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 peut pas être unifiée avec l'un l'autre pour la composition extensible. Par exemple, " problème D'Expression - comme l'extension a été unifiée dans le sous-typage, la surcharge de fonction, et les domaines de typage paramétrique.
ma position théorique est que pour la connaissance pour exister (voir la section "la centralisation est aveugle et inapte"), il y aura jamais être un modèle général qui peut imposer 100% de couverture de tous les invariants possibles dans un Turing-langage informatique complet. Pour que la connaissance existe, il existe de nombreuses possibilités inattendues, c'est-à-dire le désordre et l'entropie doivent toujours s'accroître. C'est la force entropique. Prouver tous les calculs possibles d'une extension potentielle, c'est Calculer a priori toute extension possible.
C'est pourquoi le théorème de Halting existe, c'est-à-dire qu'il est indécidable si tous les programmes possibles dans un langage de programmation Turing-complete se termine. Il peut être prouvé que certaines programme se termine (un qui 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 soit pas Turing complète (par exemple via dépendante-Dactylographie). Puisque l'exigence fondamentale pour Turing-completeness est récursion sans limites , il est intuitif de comprendre comment les théorèmes d'incomplétude de Gödel et le paradoxe de Russell s'appliquent à l'extension.
une interprétation de ces théorèmes les incorpore dans une compréhension conceptuelle généralisée de la force entropique:
- théorèmes incomplets 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'adhésion pour un ensemble qui peut contenir ensemble, soit énumère le type spécifique de chaque membre ou se contient. Ainsi les ensembles ne peuvent pas être prolongés ou ils sont sans limite de récursion. Par exemple, l'ensemble de tout ce qui n'est pas une théière, se comprend, qui se comprend, qui 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.-à-d. permet tous les types Non spécifiés) et ne permet pas l'extension sans limite. C'est l'ensemble des ensembles qui ne sont pas membres de m'. Cette incapacité à être à la fois cohérent et complètement énuméré sur toute extension possible, est Gödel incompleteness théorèmes.
- Liskov Substition Principle : en général, il est un problème indécidable si un ensemble est le sous-ensemble d'un autre, c.-à-d. héritage est généralement indécidable.
- Linsky Referencing : il est indécidable ce que le calcul de quelque chose est, quand il est décrit ou perçu, c'est-à-dire que la perception (la réalité) n'a pas de point de référence absolu.
- théorème de Coase : il n'y a pas de point de référence externe, donc toute barrière aux possibilités externes illimitées échouera.
- deuxième loi de la thermodynamique : l'univers entier (un système fermé, c.-à-d. tout) les tendances au désordre maximum, c.-à-d. indépendant maximum possibilité.
la substituabilité est un principe de la programmation orientée objet selon lequel, dans un programme d'ordinateur, si S est un sous-type de T, les objets de type T peuvent être remplacés par des objets de type S
faisons un exemple simple en Java:
mauvais exemple
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
le canard peut voler à cause de son oiseau, mais qu'en est-il:
public class Ostrich extends Bird{}
l'Autruche est un oiseau, Mais il ne peut pas voler, la classe autruche est un sous-type D'Oiseau de classe, mais il ne peut pas utiliser la méthode de la mouche, ce qui signifie que nous briser LSP Principe.
bon exemple
public class Bird{
}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
la LSP est une règle concernant le contrat des clases: si une classe de base satisfait à un contrat, alors par la LSP les classes dérivées 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 de base, aussi longtemps que arg est le même.
les fonctions qui utilisent des pointeurs ou des références à des classes de base doivent pouvoir utiliser des objets de classes dérivées sans le savoir.
quand j'ai lu pour la première fois à propos de LSP, j'ai supposé que c'était voulu dans un sens très strict, en l'assimilant essentiellement à l'implémentation d'interface et à la distribution sans risque. Ce qui veut dire que la PSL est assurée ou non par la langue elle-même. Par exemple, dans ce sens strict, le carton à trois est certainement substituable à Board, en ce qui concerne le compilateur.
après avoir lu plus sur le concept même si j'ai constaté que la PSL est généralement interprétée 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é à type-sécurité. L'adhérence à LSP est également testable en sondant le comportement réel des objets. C'est, en examinant les impact de l'état d'un objet et des arguments de méthode sur les résultats des appels de méthode, ou les types d'exceptions lancées de l'objet.
retour à l'exemple, en théorie les méthodes de conseil peut être fait pour travailler très bien sur la planche à trois. 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 la fonctionnalité que ThreeDBoard est destiné à ajouter.
avec ces connaissances en main, l'évaluation de l'adhérence aux 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.
un exemple important de utilisation de LSP est dans software testing .
si j'ai une classe A qui est une sous-classe de B conforme à la norme LSP, je peux réutiliser la suite d'essai de B pour l'essai A.
pour tester complètement 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 superclass B.
une façon de réaliser est-ce en construisant ce McGregor appelle une "hiérarchie parallèle pour les tests": ma classe ATest
héritera de BTest
. Une certaine forme d'injection est alors nécessaire pour s'assurer que le cas d'essai 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 super-test pour toutes les implémentations de la sous-classe est en fait un moyen de tester que ces implémentations de la sous-classe sont conformes aux LSP. Ainsi, on peut également faire valoir qu'un devrait exécuter la série d'essais superclass 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 y a une liste de contrôle pour déterminer si oui ou non vous violez Liskov.
- si vous violez L'un des articles suivants -> vous violez Liskov.
- si vous ne violez aucune - > cant conclure quoi que ce soit.
liste de Vérification:
- aucune nouvelle exception ne doit être jetée dans la classe dérivée : si votre classe de base a lancé ArgumentNullException alors vos sous-classes n'étaient autorisées à lancer que des exceptions de type ArgumentNullException ou des exceptions dérivées de ArgumentNullException. Lancer IndexOutOfRangeException est une violation de Liskov.
- les conditions préalables ne peuvent pas être renforcées : supposons que votre classe de base travaille avec un membre int. Maintenant, votre sous-type exige que l'int soit positif. Ceci est renforcé pré-conditions, et maintenant n'importe quel code qui a fonctionné parfaitement bien avant avec des inversions négatives est cassé.
- Les Post-conditions ne peuvent pas être affaiblies : supposons que votre classe de base exige que toutes les connexions à la base de données soient fermées avant que la méthode ne soit retournée. Dans votre sous-classe, vous annulez cette méthode et vous ouvrez la connexion pour la réutiliser. Vous avez affaibli les post-conditions de cette méthode.
- Invariants doivent être préservés : Le plus difficile et douloureuse contrainte à respecter. Invariant sont parfois cachés dans la classe de base et la seule façon de les révéler, c'est de lire le code de la classe de base. Fondamentalement, vous devez être sûr lorsque vous outrepassez une méthode quelque chose d'immuable doit rester inchangé après votre méthode outrepassée exécuté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 de L'histoire : lorsque vous outrepassez 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 étant non modifiable (private set) mais sous-Type 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 y a 2 autres articles: la Contravariance des arguments de méthode et Covariance des types de retour . Mais ce n'est pas possible en C# (je suis un développeur de C#) donc je ne se soucient d'eux.
référence:
- http://www.ckode.dk/programming/solid-principles-part-3-liskovs-substitution-principle /
- https://softwareengineering.stackexchange.com/questions/187613/how-does-strengthening-of-pre-conditions-and-weakening-of-post-conditions-violat
- https://softwareengineering.stackexchange.com/questions/170189/how-to-verify-the-liskov-substitution-principle-in-an-inheritance-hierarchy
je suppose que tout le monde a en quelque sorte couvert ce que LSP est techniquement: vous voulez fondamentalement être en mesure de abstraire loin des détails de sous-type et d'utiliser des supertypes en toute sécurité.
donc Liskov a 3 règles sous-jacentes:
-
Signature " de la Règle : Il doit être valide la mise en œuvre de chaque opération de la supertype dans le sous-type du point de vue syntaxique. Quelque chose qu'un compilateur pourra vérifier pour vous. Il y a une petite règle sur le lancer moins d'exceptions et être au moins aussi accessible que les méthodes supertype.
-
Méthodes Règle: La mise en œuvre de ces opérations est sémantiquement son.
- conditions préalables plus faibles : les fonctions de sous-type devraient prendre au moins ce que le supertype a pris comme entrée, sinon plus.
- Postconditions plus fortes: elles devraient produire un sous-ensemble de la sortie des méthodes supertype produites.
-
Propriétés de la Règle : Cela va au-delà des appels de fonction.
- 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: habituellement quelque chose à voir avec l'immutabilité ou le genre d'États dans lesquels l'objet peut être. Ou peut-être que l'objet ne fait que croître et ne rétrécit jamais, donc les méthodes des sous-types ne devraient pas le faire.
toutes ces propriétés doivent être préservées et la fonctionnalité de sous-type supplémentaire ne devrait pas violer les propriétés de supertype.
si ces trois choses sont prises en charge, vous avez abstrait loin de la substance sous-jacente et vous écrivez le code vaguement couplé.
Source: développement de programmes en Java-Barbara Liskov
Cette formulation de la LSP est 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éfini en termes de T, le comportement de P est inchangé lorsque o1 est substitué à o2, alors S est un sous-type de T.""
ce qui signifie essentiellement que S est une autre mise en œuvre complètement encapsulée de la même chose que T. Et je pourrais être audacieux et décider que la performance fait partie du comportement de P...
donc, fondamentalement, toute utilisation de reliure tardive viole la LSP. C'est tout le but de OO d'obtenir un comportement différent quand on substitue un objet d'un type à un autre!
la formulation Citée par wikipedia est meilleure puisque la propriété dépend du contexte et n'inclut pas nécessairement l'ensemble du comportement du programme.
Long bref, laissons les rectangles rectangles et les 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.
disons que vous avez une base ItemsRepository.
class ItemsRepository
{
/**
* @return int Returns number of deleted rows
*/
public function delete()
{
// perform a delete query
$numberOfDeletedRows = 10;
return $numberOfDeletedRows;
}
}
et une sous-classe l'étendant:
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;
}
}
alors vous pourriez avoir un Client travailler avec L'API de base ItemsRepository et compter dessus.
/**
* 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 cassé lorsque remplaçant classe parent classe avec une sous-classe casse 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 :(
}
}
certains addendum:
je me demande pourquoi personne n'a écrit sur L'Invariant , les conditions préalables et les conditions de poste de la classe de base qui doivent être obéies par les classes dérivées.
Pour qu'une classe d dérivée soit entièrement sustituable par la classe de Base B, La classe D doit obéir à certaines conditions:
- les variantes internes de la classe de base doivent être conservées par la classe dérivée
- les conditions préalables de la classe de base ne doivent pas être renforcée par la classe dérivée
- les conditions postérieures à 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, "est une" relation ne doit être obéi que lorsque certaines règles sont obéies par le sous-type. Ces règles, sous forme d'invariants, de précodes et de postconditions, devraient être décidé par un "" contrat de conception " ".
D'autres discussions sur ce disponible sur mon blog: Liskov principe de Substitution
dans une phrase très simple, nous pouvons dire:
la classe d'enfants ne doit pas violer ses caractéristiques de classe de base. Elle doit pouvoir être avec elle. On peut dire que c'est comme un sous-typage.
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é. Cela fonctionne en introduisant des effets secondaires. Mais si le rectangle avait un setSize (hauteur, largeur) avec une condition préalable 0 < hauteur et 0 < Largeur. La méthode du sous-type dérivé nécessite une hauteur = = largeur; une condition préalable plus forte (et qui viole lsp). Cela montre que bien que le carré est un rectangle, il n'est pas un sous-Type valide parce que la condition préalable est renforcée. Le travail autour (en général une mauvaise chose) cause un effet secondaire et cela affaiblit la condition de poteau (qui viole lsp). setWidth sur la base de la post condition 0 < largeur. Le dérivé l'affaiblit avec hauteur = = largeur.
par conséquent, un carré redimensionnable n'est pas un rectangle redimensionnable.
je vois des rectangles et des carrés dans chaque réponse, et comment violer la LSP.
je voudrais montrer comment le LSP peut être conforme à un exemple du monde 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 à la 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 !
}
}
maintenant les sous-types ne peuvent pas être utilisés de la même manière car ils ne produisent plus le même résultat.
est-ce que la mise en œuvre de la planche à trois en termes d'une rangée de planches serait aussi utile?
peut-être voulez-vous traiter les tranches de carton fileté dans divers plans comme une planche. Dans ce cas, vous pouvez vouloir abstraire une interface (ou une classe abstraite) pour la carte pour permettre des implémentations multiples.
en termes d'interface externe, vous pourriez vouloir prendre en compte une interface de carte à la fois pour TwoDBoard et ThreeDBoard (bien qu'aucun des ci-dessus les méthodes d'ajustement).
je vous encourage à lire l'article: violant le principe de substitution de Liskov (LSP) .
vous pouvez y trouver une explication de ce qu'est 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 classe plus sûre.
l'explication la plus claire pour LSP que j'ai trouvé jusqu'à présent a été "le principe de substitution de Liskov dit que l'objet d'une classe dérivée devrait être en mesure de remplacer un objet de la classe de base sans apporter aucune erreur dans le système ou modifier le comportement de la classe de base" de ici . L'article donne un exemple de code pour violer la LSP et la corriger.
LISKOV principe de SUBSTITUTION (de Mark Seemann Livre) stipule que nous devrions être en mesure de remplacer une mise en œuvre d'une interface avec une autre sans briser client ou la mise en œuvre.C'est ce principe qui permet de répondre aux besoins qui se présenteront à l'avenir, même si nous ne pouvons pas 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 se décompose (en fait, si c'est un ordinateur portable, il peut même fonctionner sur piles pour une période de temps). Toutefois, avec les logiciels, 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 n'a "rien."Il s'agit d'un motif connu sous le nom D'objet nul,[4] qui correspond grosso modo à débrancher l'ordinateur du mur. Parce que nous utilisons le couplage lâche, nous peut remplacer une implémentation réelle par quelque chose qui ne fait rien sans causer de problèmes.
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 parce que sa largeur est la même longueur que sa hauteur. Faisons une classe Square
aussi bien basée sur cette info:
class Square extends Rectangle {
setDimensions(width, height){
assert(width == height);
super.setDimensions(width, height);
}
}
si nous remplaçons le Rectangle
par Square
dans notre premier code, alors il se cassera:
r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);
c'est parce que le Square
a une nouvelle condition préalable que nous n'avions pas dans la classe Rectangle
: width == height
. Selon LSP, les instances Rectangle
doivent être substituables avec les instances de la sous-classe Rectangle
. Ceci est dû au fait que ces instances passent le contrôle de type pour les instances Rectangle
et qu'elles causeront des erreurs inattendues dans votre code.
ce fut un exemple pour le "les conditions préalables ne peuvent pas être renforcées dans un sous-type" partie dans le article wiki . Donc, pour résumer, violer LSP va probablement causer des erreurs dans votre code à un moment donné.
principe de substitution de Liskov (LSP)
tout le temps nous concevons un module de programme et nous créons une classe hiérarchie. Puis nous étendons certaines classes en créant des classes dérivées classe.
nous devons nous assurer que les nouvelles classes dérivées s'étendent sans remplacez les fonctionnalités des anciennes classes. Autrement, les nouvelles classes peut produire des effets indésirables lorsqu'ils sont utilisés dans l'existant programme module.
le principe de substitution de Liskov stipule que si un module de programme est en utilisant une classe de Base, alors la référence à la classe de Base peut être remplacé par une classe Dérivée sans affecter la fonctionnalité de le module de programme.
exemple:
ci-dessous est l'exemple classique pour lequel le principe de substitution de Liskov est violé. Dans l'exemple, 2 classes sont utilisé: Rectangle et carré. Supposons que l'objet Rectangle est utilisé quelque part dans l'application. Nous étendons l'application et ajoutons la classe carrée. La classe carrée est retournée par un motif d'usine, basé sur certaines conditions et nous ne savons pas exactement quel type d'objet sera retourné. Mais on sait que c'est un Rectangle. Nous obtenons l'objet rectangle, définissons la largeur à 5 et la hauteur à 10 et obtenons la zone. Pour un rectangle de largeur 5 et hauteur 10, la surface doit être de 50. Au lieu de cela, le résultat sera de 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 est juste une extension du principe de fermeture ouverte et il cela signifie que nous devons nous assurer que les nouvelles classes dérivées s'étendent les classes de base sans changer leur comportement.
Voir aussi: Ouvrir Fermer Principe
Some concepts similaires pour une meilleure structure: Convention sur la configuration
le principe de substitution de Likov stipule que si un module de programme utilise une classe de Base, alors 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.
les types dérivés D'intention doivent être complètement substituables aux types de base.
Exemple - Co-variant les types de retour en java.
voici un extrait de ce post qui clarifie les choses bien:
[..] pour comprendre certains principes, il est important de se rendre compte qu'ils ont été violés. C'est 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 identifié votre les abstractions sont fausses.
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 de la LSP? Oui. C'est parce que le compte du contrat nous dit qu'un compte serait retirée, mais ce n'est pas toujours le cas. Alors, que dois-je faire pour corriger cela? Je viens de modifier 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à, le contrat est respecté.
cette violation subtile impose souvent un client avec la capacité de faire la différence entre des objets utilisés. Par exemple, compte tenu du contrat du premier compte, il pourrait ressembler à ce qui suit:
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 de la clôture ouverte [c'est-à-dire, pour l'exigence de retrait d'argent. Parce qu'on ne sait jamais ce qui se passe si un objet violant le contrat n'a pas assez d'argent. Probablement il ne retourne rien, probablement une exception sera lancée. Donc vous devez vérifier si elle hasEnoughMoney()
-- qui ne fait pas partie d'une interface. Donc ce contrôle forcé dépendant de la classe de béton est une violation de L'OCP].
Ce point également, les adresses d'un malentendu que je rencontre assez souvent sur LSP violation. Ça dit que "si le comportement d'un parent a changé chez un enfant, alors, ça viole la LSP."Cependant, ce n'est pas le cas - tant qu'un enfant ne viole pas le contrat de son parent.