Si un carré est un type de rectangle, pourquoi ne peut-il pas hériter d'un rectangle? Ou pourquoi est-ce un mauvais design?
J'ai entendu des gens dire:
Si vous faites que les carrés dérivent du rectangle, alors un carré devrait être utilisable partout où vous vous attendez à un rectangle
Quel est le problème ici? Et pourquoi Square serait-il utilisable partout où vous vous attendiez à un rectangle? Cela ne serait utilisable que si nous créons l'objet Square et si nous remplaçons les méthodes SetWidth et SetHeight pour Square, pourquoi y aurait-il un problème?
Si vous aviez des méthodes SetWidth et SetHeight sur votre classe de base Rectangle et si votre référence Rectangle pointait vers un carré, SetWidth et SetHeight n'ont pas de sens, car définir l'une modifierait l'autre en conséquence. Dans ce cas, Square échoue au test de substitution de Liskov avec Rectangle et l'abstraction d'avoir Square hérité de Rectangle est mauvaise.
Quelqu'un peut-il expliquer les arguments ci-dessus? Encore une fois, si nous annulons les méthodes SetWidth et SetHeight dans Square, cela ne résoudrait-il pas le problème?
J'ai aussi entendu / lu:
Le vrai problème est que nous ne modélisons pas des rectangles, mais plutôt des "rectangles redéfinissables", c'est-à-dire des rectangles dont la largeur ou la hauteur peut être modifiée après la création (et nous considérons toujours qu'il s'agit du même objet). Si nous examinons la classe de rectangle de cette manière, il est clair qu'un carré n'est pas un "rectangle transformable", car un carré ne peut pas être remodelé et reste un carré (en général). Mathématiquement, nous ne voyons pas le problème car la mutabilité n'a même pas de sens dans un contexte mathématique
Ici, je pense que "ré-dimensionnable" est le terme correct. Les rectangles sont "redimensionnables", de même que les carrés. Est-ce que je manque quelque chose dans l'argument ci-dessus? Un carré peut être redimensionné comme n'importe quel rectangle.
la source
Why do we even need Square
? C'est comme avoir deux stylos. Un stylo bleu et un stylo rouge bleu, jaune ou vert. Le stylo bleu est redondant - encore plus dans le cas du carré car il ne présente aucun avantage financier.Réponses:
Fondamentalement, nous voulons que les choses se comportent de manière raisonnable.
Considérez le problème suivant:
On me donne un groupe de rectangles et je veux augmenter leur surface de 10%. Donc, ce que je fais est que je règle la longueur du rectangle à 1,1 fois ce qu'il était auparavant.
Maintenant, dans ce cas, la longueur de tous mes rectangles a été augmentée de 10%, ce qui augmentera leur surface de 10%. Malheureusement, quelqu'un m'a passé un mélange de carrés et de rectangles, et lorsque la longueur du rectangle a été modifiée, la largeur l'a été également.
Mes tests unitaires réussissent parce que j'ai écrit tous mes tests unitaires pour utiliser une collection de rectangles. J'ai maintenant introduit un bogue subtil dans mon application qui peut passer inaperçu pendant des mois.
Pire encore, Jim de la comptabilité voit ma méthode et écrit un autre code qui utilise le fait que s’il passe des carrés à ma méthode, il obtient une très belle augmentation de 21% en taille. Jim est heureux et personne n'est plus sage.
Jim est promu pour son excellent travail dans une division différente. Alfred rejoint la société en tant que junior. Dans son premier rapport de bogue, Jill de Advertising a indiqué que le fait de passer des carrés à cette méthode entraîne une augmentation de 21% et veut que le bogue soit corrigé. Alfred voit que les carrés et les rectangles sont utilisés partout dans le code et se rend compte que casser la chaîne de l'héritage est impossible. De plus, il n'a pas accès au code source de la comptabilité. Alors Alfred corrige le bogue comme ceci:
Alfred est satisfait de ses compétences en cyber-piratage et Jill signale que le bogue est corrigé.
Le mois prochain, personne ne sera payé car la comptabilité dépendait de la possibilité de passer des carrés à la
IncreaseRectangleSizeByTenPercent
méthode et d'obtenir une augmentation de surface de 21%. Toute la société passe en mode "priorité 1 bugfix" pour retrouver la source du problème. Ils attribuent le problème à la solution d'Alfred. Ils savent qu'ils doivent garder la comptabilité et la publicité heureuses. Donc, ils résolvent le problème en identifiant l'utilisateur avec l'appel de méthode comme suit:Et ainsi de suite.
Cette anecdote est basée sur des situations réelles auxquelles sont confrontés quotidiennement les programmeurs. Les violations du principe de substitution de Liskov peuvent introduire des bogues très subtils qui ne sont détectés que des années après leur rédaction. À ce moment-là, la résolution de la violation annulera un tas de choses et le fait de ne pas le réparer irritera votre plus gros client.
Il existe deux moyens réalistes de résoudre ce problème.
La première consiste à rendre Rectangle immuable. Si l'utilisateur de Rectangle ne peut pas modifier les propriétés Longueur et Largeur, ce problème disparaît. Si vous souhaitez un rectangle de longueur et de largeur différentes, vous en créez un nouveau. Les carrés peuvent hériter des rectangles avec bonheur.
La deuxième méthode consiste à rompre la chaîne d'héritage entre carrés et rectangles. Si un carré est défini comme ayant une seule
SideLength
propriété et rectangles ont uneLength
etWidth
propriété et il n'y a pas d' héritage, il est impossible de briser accidentellement les choses en attendant un rectangle et d' obtenir un carré. En termes C #, vous pouvezseal
utiliser votre classe de rectangle, ce qui garantit que tous les rectangles que vous obtenez sont réellement des rectangles.Dans ce cas, j'aime bien la manière de "résoudre les problèmes avec des" objets immuables ". L'identité d'un rectangle est sa longueur et sa largeur. Il est logique que lorsque vous souhaitez modifier l'identité d'un objet, ce que vous voulez vraiment, c'est un nouvel objet. Si vous perdez un ancien client et gagnez un nouveau client, vous ne modifiez pas le
Customer.Id
champ de l'ancien client au nouveau, vous créez un nouveauCustomer
.Les violations du principe de substitution de Liskov sont courantes dans le monde réel, principalement parce que de nombreux codes sont écrits par des personnes incompétentes / soumises à des contraintes de temps / qui ne s'en soucient pas / qui font des erreurs. Cela peut conduire à de très vilains problèmes. Dans la plupart des cas, vous préférez la composition à l'héritage .
la source
Si tous vos objets sont immuables, il n'y a pas de problème. Chaque carré est aussi un rectangle. Toutes les propriétés d'un rectangle sont également des propriétés d'un carré.
Le problème commence lorsque vous ajoutez la possibilité de modifier les objets. Ou vraiment - lorsque vous commencez à passer des arguments à l'objet, pas seulement à lire des getters de propriétés.
Certaines modifications que vous pouvez apporter à un rectangle conservant tous les invariants de votre classe Rectangle, mais pas tous les invariants de carrés, comme la modification de la largeur ou de la hauteur. Soudain, le comportement d’un rectangle n’est pas seulement ses propriétés, mais aussi ses modifications possibles. Ce n'est pas seulement ce que vous obtenez du rectangle, c'est aussi ce que vous pouvez insérer .
Si votre rectangle a une méthode
setWidth
documentée comme modifiant la largeur sans modifier la hauteur, Square ne peut pas avoir une méthode compatible. Si vous modifiez la largeur et non la hauteur, le résultat n'est plus un carré valide. Si vous choisissez de modifier la largeur et la hauteur du carré lors de l'utilisationsetWidth
, vous n'implémentez pas la spécification de RectanglesetWidth
. Vous ne pouvez tout simplement pas gagner.Lorsque vous regardez ce que vous pouvez "mettre" dans un rectangle et un carré, quels messages vous pouvez leur envoyer, vous constaterez probablement que tout message que vous pouvez envoyer valablement à un carré, vous pouvez également l'envoyer à un rectangle.
C'est une question de co-variance par opposition à une contre-variance.
Les méthodes d'une sous-classe appropriée, celle dans laquelle des instances peuvent être utilisées dans tous les cas où la super-classe est attendue, exigent que chaque méthode:
Revenons donc à Rectangle et à Square: savoir si Square peut être une sous-classe de Rectangle dépend entièrement des méthodes dont dispose Rectangle.
Si Rectangle a des réglages individuels pour la largeur et la hauteur, Square ne fera pas une bonne sous-classe.
De même, si vous faites en sorte que certaines méthodes soient des variantes dans les arguments, comme avoir
compareTo(Rectangle)
Rectangle etcompareTo(Square)
Square, vous aurez un problème pour utiliser un carré en tant que rectangle.Si vous concevez votre carré et votre rectangle pour qu'ils soient compatibles, cela fonctionnera probablement, mais ils devraient être développés ensemble, ou je parie que cela ne fonctionnera pas.
la source
Il y a beaucoup de bonnes réponses ici; La réponse de Stephen en particulier illustre bien le fait que les violations du principe de substitution entraînent des conflits réels entre les équipes.
J'ai pensé que je pourrais parler brièvement du problème spécifique des rectangles et des carrés, plutôt que de l'utiliser comme métaphore pour d'autres violations du LSP.
Il y a un problème supplémentaire avec square-is-a-special-kind-of-rectangle qui est rarement mentionné, à savoir: pourquoi nous arrêtons-nous avec des carrés et des rectangles ? Si nous sommes disposés à dire qu'un carré est un type spécial de rectangle, alors nous devrions aussi être prêts à dire:
Qu'est-ce que toutes les relations devraient être ici? Les langages basés sur l'héritage de classes tels que C # ou Java n'ont pas été conçus pour représenter ce type de relations complexes avec de multiples types de contraintes. Il est préférable d'éviter simplement la question en n'essayant pas de représenter toutes ces choses en tant que classes avec des relations de sous-typage.
la source
IShape
type peut inclure un cadre de sélection, être dessiné, mis à l'échelle et sérialisé, ainsi qu'unIPolygon
sous - type avec une méthode permettant de signaler le nombre de sommets et une méthode permettant de renvoyer unIEnumerable<Point>
. On pourrait alors avoir unIQuadrilateral
sous - type qui dérive deIPolygon
,IRhombus
etIRectangle
dérive de cela, etISquare
dérive deIRhombus
etIRectangle
. La mutabilité jetterait tout par la fenêtre, et l'héritage multiple ne fonctionne pas avec les classes, mais je pense que ça va avec les interfaces immuables.IRhombus
garantit-on que tous lesPoint
retours de ce qui estEnumerable<Point>
défini parIPolygon
correspondent à des arêtes de longueurs égales? Parce que laIRhombus
seule implémentation de l' interface ne garantit pas qu'un objet concret est un losange, l'héritage ne peut pas être la réponse.D'un point de vue mathématique, un carré est un rectangle. Si un mathématicien modifie le carré pour qu'il n'adhère plus au contrat du carré, il se transforme en un rectangle.
Mais dans la conception OO, c'est un problème. Un objet est ce qu'il est et cela inclut les comportements ainsi que l'état. Si je tiens un objet carré, mais que quelqu'un d'autre le modifie pour le transformer en rectangle, le contrat du carré ne sera violé par aucune faute de ma part. Cela provoque toutes sortes de mauvaises choses.
Le facteur clé ici est la mutabilité . Une forme peut-elle changer une fois construite?
Mutable: si les formes sont autorisées à changer une fois construites, un carré ne peut avoir de relation is-a avec un rectangle. Le contrat d'un rectangle inclut la contrainte que les côtés opposés doivent être de même longueur, mais que les côtés adjacents ne doivent pas nécessairement l'être. Le carré doit avoir quatre côtés égaux. La modification d'un carré via une interface rectangulaire peut violer le contrat carré.
Immuable: si les formes ne peuvent pas changer une fois construites, un objet carré doit également toujours remplir le contrat rectangle. Un carré peut avoir une relation is-a avec un rectangle.
Dans les deux cas, il est possible de demander à un carré de produire une nouvelle forme en fonction de son état avec un ou plusieurs changements. Par exemple, on pourrait dire "créer un nouveau rectangle basé sur ce carré, sauf que les côtés opposés A et C sont deux fois plus longs." Depuis la construction d'un nouvel objet, la place d'origine continue de respecter ses contrats.
la source
This is one of those cases where the real world is not able to be modeled in a computer 100%
. Pourquoi Nous pouvons toujours avoir un modèle fonctionnel d'un carré et d'un rectangle. La seule conséquence est que nous devons rechercher une construction plus simple à abstraire sur ces deux objets.Parce que cela fait partie de ce que signifie être un sous-type (voir aussi: principe de substitution de Liskov). Vous pouvez faire, vous devez être capable de faire ceci:
En fait, vous le faites tout le temps (parfois même implicitement) lorsque vous utilisez la POO.
Parce que vous ne pouvez pas raisonnablement remplacer ceux pour
Square
. Parce qu'un carré ne peut pas "être redimensionné comme n'importe quel rectangle". Lorsque la hauteur d'un rectangle change, la largeur reste la même. Mais lorsque la hauteur d'un carré change, la largeur doit changer en conséquence. Le problème ne se limite pas à être redimensionnable, il l’est indépendamment dans les deux dimensions.la source
Rect r = s;
ligne, vous pouvez simplementdoSomethingWith(s)
et le runtime utilisera tous les appelss
pour résoudre toutes lesSquare
méthodes virtuelles .setWidth
etsetHeight
pour changer la largeur et la hauteur.Ce que vous décrivez va à l'encontre de ce que l'on appelle le principe de substitution de Liskov . L'idée de base du LSP est que, chaque fois que vous utilisez une instance d'une classe particulière, vous devriez toujours pouvoir permuter une instance d'une sous-classe de cette classe sans introduire de bogues.
Le problème du rectangle carré n'est pas vraiment un très bon moyen de présenter Liskov. Il tente d'expliquer un principe général à l'aide d'un exemple assez subtil et va à l'encontre de l'une des définitions intuitives les plus courantes de toutes les mathématiques. Certains appellent cela le problème de l’Ellipse-Circle, mais c’est un peu mieux dans la mesure du possible. Une meilleure approche consiste à prendre un peu de recul, en utilisant ce que j'appelle le problème Parallélogramme-Rectangle. Cela rend les choses beaucoup plus faciles à comprendre.
Un parallélogramme est un quadrilatère avec deux paires de côtés parallèles. Il a également deux paires d'angles congruents. Il n’est pas difficile d’imaginer un objet de parallélogramme dans ce sens:
Une façon courante de penser à un rectangle est d'utiliser un parallélogramme avec des angles droits. À première vue, cela peut sembler faire de Rectangle un bon candidat pour hériter de Parallelogram , de sorte que vous puissiez réutiliser tout ce code délicieux. Pourtant:
Pourquoi ces deux fonctions introduisent-elles des bugs dans Rectangle? Le problème est que vous ne pouvez pas changer les angles dans un rectangle : ils sont définis comme étant toujours à 90 degrés et cette interface ne fonctionne donc pas pour Rectangle héritant de Parallelogram. Si j'échange un rectangle en code qui attend un parallélogramme et que ce code tente de changer l'angle, il y aura presque certainement des bogues. Nous avons pris quelque chose qui était inscriptible dans la sous-classe et l'avons rendu en lecture seule, et c'est une violation de Liskov.
Maintenant, comment cela s'applique-t-il aux carrés et aux rectangles?
Lorsque nous disons que vous pouvez définir une valeur, nous entendons généralement quelque chose d'un peu plus fort que le simple fait de pouvoir y écrire une valeur. Nous impliquons un certain degré d'exclusivité: si vous définissez une valeur, alors sauf circonstances exceptionnelles, elle restera à cette valeur jusqu'à ce que vous la définissiez à nouveau. Il y a beaucoup d'utilisations pour les valeurs qui peuvent être écrites mais ne restent pas définies, mais il y a aussi de nombreux cas qui dépendent d'une valeur qui reste telle quelle. Et c'est là que nous rencontrons un autre problème.
Notre classe Square a hérité des bugs de Rectangle, mais il en a de nouveaux. Le problème avec setSideA et setSideB est qu’aucun de ceux-ci n’est véritablement paramétrable: vous pouvez toujours écrire une valeur dans l’une ou l’autre, mais celle-ci changera si vous écrivez l’autre. Si j'échange ceci avec un parallélogramme en code qui dépend de la possibilité de définir des côtés indépendamment les uns des autres, cela va paniquer.
C’est là le problème, et c’est la raison pour laquelle l’utilisation de Rectangle-Carré comme introduction à Liskov pose problème. Rectangle-Square dépend de la différence entre savoir écrire et définir , et c'est une différence beaucoup plus subtile que de pouvoir définir quelque chose plutôt que de le lire en lecture seule. Rectangle-Square a toujours de la valeur à titre d'exemple, car il documente un piège assez commun qui doit être surveillé, mais il ne doit pas être utilisé comme exemple d' introduction . Laissez l’apprenant se familiariser d'abord avec les bases, puis lancez-lui quelque chose de plus difficile.
la source
Le sous-typage concerne le comportement.
Pour que type
B
soit un sous-typeA
, il doit prendre en charge toutes les opérations prises enA
charge par la même sémantique (langage sophistiqué pour "comportement"). L'utilisation de la justification selon laquelle chaque B est un A ne fonctionne pas - la compatibilité des comportements a le dernier mot. La plupart du temps, "B est une sorte de A" et "B se comporte comme A", mais pas toujours .Un exemple:
Considérons l'ensemble des nombres réels. Dans toutes les langues, on peut les attendre à soutenir les opérations
+
,-
,*
et/
. Considérons maintenant l'ensemble des entiers positifs ({1, 2, 3, ...}). Il est clair que chaque entier positif est aussi un nombre réel. Mais le type d’entiers positifs est-il un sous-type du type de nombres réels? Examinons les quatre opérations et voyons si les nombres entiers positifs se comportent de la même manière que les nombres réels:+
: Nous pouvons ajouter des entiers positifs sans problèmes.-
: Toutes les soustractions d’entiers positifs ne donnent pas lieu à des entiers positifs. Par exemple3 - 5
.*
: Nous pouvons multiplier des entiers positifs sans problèmes./
: On ne peut pas toujours diviser les entiers positifs et obtenir un entier positif. Par exemple5 / 3
.Ainsi, bien que les entiers positifs soient un sous-ensemble de nombres réels, ils ne sont pas un sous-type. Un argument similaire peut être utilisé pour les entiers de taille finie. Il est clair que chaque entier de 32 bits est également un entier de 64 bits, mais
32_BIT_MAX + 1
vous donnera des résultats différents pour chaque type. Donc, si je vous ai donné un programme et que vous avez changé le type de chaque variable entière 32 bits en nombres entiers 64 bits, il y a de fortes chances que le programme se comporte différemment (ce qui veut dire presque toujours mal ).Bien sûr, vous pouvez définir
+
des entiers de 32 bits pour obtenir un entier de 64 bits, mais vous devez maintenant réserver un espace de 64 bits à chaque fois que vous ajoutez deux nombres de 32 bits. Cela peut être acceptable ou non pour vous en fonction de vos besoins en mémoire.Pourquoi est-ce important?
Il est important que les programmes soient corrects. C'est sans doute la propriété la plus importante pour un programme. Si un programme est correct pour un type donné
A
, le seul moyen de garantir que ce programme restera correct pour certains sousB
- types est deB
se comporter de la mêmeA
manière.Donc, vous avez le type de
Rectangles
, dont la spécification dit que ses côtés peuvent être modifiés indépendamment. Vous avez écrit certains programmes qui utilisentRectangles
et supposent que l'implémentation suit la spécification. Ensuite, vous avez introduit un sous-type appeléSquare
dont les côtés ne peuvent pas être redimensionnés indépendamment. En conséquence, la plupart des programmes qui redimensionnent des rectangles seront désormais erronés.la source
Tout d’abord, demandez-vous pourquoi, à votre avis, un carré est un rectangle.
Bien sûr, la plupart des gens l’ont appris à l’école primaire, et cela paraît évident. Un rectangle est une forme à 4 côtés avec des angles de 90 degrés et un carré remplit toutes ces propriétés. Donc, un carré n'est-il pas un rectangle?
Cependant, le fait est que tout dépend de vos critères initiaux pour grouper des objets, du contexte dans lequel vous regardez ces objets. Dans la géométrie, les formes sont classées en fonction des propriétés de leurs points, lignes et anges.
Donc, avant même de dire "un carré est un type de rectangle", vous devez vous demander si cela est basé sur des critères qui me tiennent à cœur .
Dans la grande majorité des cas, ce ne sera pas du tout ce qui compte pour vous. La majorité des systèmes qui modélisent des formes, tels que les interfaces graphiques, les graphiques et les jeux vidéo, ne sont pas principalement concernés par le regroupement géométrique d'un objet, mais par son comportement. Avez-vous déjà travaillé sur un système qui importait qu'un carré soit un type de rectangle au sens géométrique. Qu'est-ce que cela vous donnerait, sachant qu'il a 4 côtés et des angles de 90 degrés?
Vous ne modélisez pas un système statique, vous modélisez un système dynamique dans lequel des événements vont se produire (des formes vont être créées, détruites, modifiées, dessinées, etc.). Dans ce contexte, vous vous préoccupez du comportement partagé entre les objets, car votre préoccupation principale est de savoir ce que vous pouvez faire avec une forme, quelles règles doivent être respectées pour conserver un système cohérent.
Dans ce contexte, un carré n'est certainement pas un rectangle , car les règles qui régissent sa modification ne sont pas les mêmes que le rectangle. Donc, ils ne sont pas le même genre de chose.
Dans ce cas, ne les modélisez pas comme tels. Pourquoi voudrais-tu? Cela ne vous rapporte rien d'autre qu'une restriction inutile.
Si vous faites cela, vous déclarez pratiquement dans le code que ce n'est pas la même chose. Votre code indiquerait un carré se comporte de cette façon et un rectangle se comporte de cette façon, mais ils sont toujours les mêmes.
Ils ne sont clairement pas les mêmes dans le contexte qui vous tient à cœur, car vous venez de définir deux comportements différents. Alors, pourquoi prétendre qu'ils sont identiques s'ils ne le sont que dans un contexte qui ne vous intéresse pas?
Cela met en évidence un problème important lorsque les développeurs se rendent dans un domaine qu'ils souhaitent modéliser. Il est si important de clarifier le contexte qui vous intéresse avant de commencer à penser aux objets du domaine. Quel aspect vous intéresse-t-il? Il y a des milliers d'années, les Grecs se souciaient des propriétés communes des lignes et des anges de formes et les regroupaient en fonction de celles-ci. Cela ne signifie pas que vous êtes obligé de continuer ce regroupement s'il ne vous intéresse pas (ce qui, dans 99% des cas, modéliserait un logiciel sans vous en soucier).
Beaucoup de réponses à cette question se concentrent sur le sous-typage étant donné le comportement de regroupement, ce qui leur donne les règles .
Mais il est très important de comprendre que vous ne le faites pas simplement pour suivre les règles. Vous le faites parce que dans la grande majorité des cas, c'est également ce qui compte pour vous. Vous ne vous souciez pas de savoir si un carré et un rectangle partagent les mêmes anges internes. Vous vous souciez de ce qu'ils peuvent faire tout en restant des carrés et des rectangles. Vous vous souciez du comportement des objets car vous modélisez un système axé sur la modification du système en fonction des règles de comportement des objets.
la source
Rectangle
servent uniquement à représenter des valeurs , il est alors possible qu'une classeSquare
hérite deRectangle
son contrat et s'en conforme pleinement. Malheureusement, de nombreuses langues ne font aucune distinction entre les variables qui encapsulent des valeurs et celles qui identifient des entités.Square
type immuable qui hérite d'unRectnagle
type immuable pourrait être utile s'il existe des types d'opérations qui ne peuvent être effectuées que sur des carrés. En tant qu'exemple réaliste du concept, considérons unReadableMatrix
type [type de base un tableau rectangulaire qui peut être stocké de différentes manières, y compris de manière éparse], et uneComputeDeterminant
méthode. Il serait peut-être logique de neComputeDeterminant
travailler qu'avec unReadableSquareMatrix
type dérivé deReadableMatrix
ce que je considérerais comme un exempleSquare
dérivé de aRectangle
.Le problème réside dans le fait de penser que si les choses sont liées d'une manière ou d'une autre dans la réalité, elles doivent l'être exactement de la même manière après la modélisation.
Le plus important dans la modélisation est d'identifier les attributs communs et les comportements communs, de les définir dans la classe de base et d'ajouter des attributs supplémentaires dans les classes enfants.
Le problème avec votre exemple est que c'est complètement abstrait. Tant que personne ne sait pour quoi vous comptez utiliser ces classes, il est difficile de deviner quel design vous devriez faire. Mais si vous voulez vraiment avoir uniquement la hauteur, la largeur et le redimensionnement, il serait plus logique de:
width
paramètre etresize(double factor)
redimensionner la largeur par le facteur donnéheight
, et remplace saresize
fonction, qui appellesuper.resize
puis redimensionne la hauteur par le facteur donnéDu point de vue de la programmation, il n’ya rien dans Square, que Rectangle n’a pas. Il est inutile de créer un carré en tant que sous-classe de Rectangle.
la source
Parce que, par LSP, créer une relation d’héritage entre les deux et remplacer
setWidth
etsetHeight
s’assurer que carré a les deux comme même introduit un comportement déroutant et non intuitif. Disons que nous avons un code:Mais si la méthode
createRectangle
est retournéeSquare
, car c'est possible grâce à l'Square
héritage deRectange
. Ensuite, les attentes sont brisées. Ici, avec ce code, nous nous attendons à ce que définir largeur ou hauteur ne provoque respectivement que le changement de largeur ou de hauteur. Le principe de la programmation orientée objet est que, lorsque vous travaillez avec la superclasse, vous ne connaissez aucune sous-classe. Et si la sous-classe change le comportement de manière à aller à l'encontre des attentes vis-à-vis de la super-classe, il y a de fortes chances que des bugs se produisent. Et ce genre de bugs est difficile à corriger et à corriger.L'une des idées principales sur la POO est qu'il s'agit d'un comportement, et non de données héritées (qui est également l'une des principales idées fausses de l'OMI). Et si vous regardez les carrés et les rectangles, ils n’ont pas eux-mêmes de comportement que nous pourrions relier dans une relation d’héritage.
la source
Ce que dit LSP, c'est que tout ce qui hérite
Rectangle
doit être unRectangle
. C'est-à-dire qu'il devrait faire ce que tout le mondeRectangle
fait.La documentation pour
Rectangle
est probablement écrite pour dire que le comportement d'unRectangle
nommér
est le suivant:Si votre Square n'a pas le même comportement, il ne se comporte pas comme un
Rectangle
. Donc, LSP dit qu'il ne doit pas hériter deRectangle
. Le langage ne peut pas appliquer cette règle, car il ne peut pas vous empêcher de faire quelque chose de mal dans un remplacement de méthode, mais cela ne signifie pas que "c'est OK parce que le langage me permet de remplacer les méthodes" est un argument convaincant pour le faire!Maintenant, il serait possible d’écrire la documentation de
Rectangle
telle manière que cela n’implique pas que le code ci-dessus en imprime 10, auquel cas vousSquare
pourriez peut-être être unRectangle
. Vous pouvez voir une documentation qui dit quelque chose comme "ceci est X. De plus, l'implémentation dans cette classe est Y". Dans ce cas, vous avez tout intérêt à extraire une interface de la classe et à faire la distinction entre ce que l’interface garantit et ce que la classe garantit en plus de cela. Mais quand les gens disent "un carré mutable n'est pas un rectangle mutable, alors qu'un carré immuable est un rectangle immuable", ils supposent fondamentalement que ce qui précède fait bien partie de la définition raisonnable d'un rectangle mutable.la source
Les sous-types et, par extension, la programmation OO, reposent souvent sur le principe de substitution de Liskov, selon lequel toute valeur de type A peut être utilisée lorsqu'un B est requis, si A <= B. Il s'agit en gros d'un axiome dans l'architecture OO, c'est-à-dire. il est supposé que toutes les sous-classes auront cette propriété (sinon, les sous-types sont bogués et doivent être corrigés).
Cependant, il s'avère que ce principe est soit irréaliste / non représentatif de la plupart des codes, soit même impossible à satisfaire (dans des cas non triviaux)! Ce problème, connu sous le nom de problème de rectangle carré ou de problème de cercle-ellipse ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ), est un exemple célèbre de la difficulté avec laquelle il est rempli.
Notez que nous pourrions implémenter des carrés et des rectangles de plus en plus équivalents sur le plan observationnel, mais uniquement en jetant de plus en plus de fonctionnalités jusqu'à ce que la distinction soit inutile.
Par exemple, voir http://okmij.org/ftp/Computation/Subtyping/
la source