Pourquoi Square héritant de Rectangle poserait-il problème si nous substituons les méthodes SetWidth et SetHeight?

105

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.

utilisateur793468
la source
15
Cette question semble terriblement abstraite. Il existe de nombreuses façons d'utiliser les classes et l'héritage, qu'il soit judicieux de faire en sorte qu'une classe hérite d'une classe, dépend généralement de la manière dont vous voulez utiliser ces classes. Sans cas pratique, je ne vois pas comment cette question peut apporter une réponse pertinente.
aaaaaaaaaaaa
2
Avec un peu de bon sens, il est rappelé que square est un rectangle. Par conséquent, si un objet de votre classe square ne peut pas être utilisé là où un rectangle est requis, il s'agit probablement d'un défaut de conception de l'application.
Cthulhu
7
Je pense que la meilleure question est 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.
Gusdor
2
@ eBusiness Son abstraite est ce qui en fait une bonne question d'apprentissage. Il est important de pouvoir identifier quelles utilisations du sous-typage sont mauvaises indépendamment des cas d'utilisation particuliers.
Doval
5
@Cthulhu Pas vraiment. Le sous-typage concerne le comportement et un carré mutable ne se comporte pas comme un rectangle modifiable. C'est pourquoi la métaphore "est un ..." est mauvaise.
Doval

Réponses:

189

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.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

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:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

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 IncreaseRectangleSizeByTenPercentmé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:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

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 SideLengthpropriété et rectangles ont une Lengthet Widthproprié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 pouvez sealutiliser 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.Idchamp de l'ancien client au nouveau, vous créez un nouveau Customer.

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 .

Stephen
la source
7
Liskov est une chose et le stockage en est un autre. Dans la plupart des implémentations, une instance Square héritant de Rectangle nécessitera de l'espace pour stocker deux dimensions, même si une seule est nécessaire.
el.pescado
29
Brillante utilisation d'une histoire pour illustrer le propos
Rory Hunter
29
Belle histoire mais je ne suis pas d'accord. Le cas d'utilisation était: changer la surface d'un rectangle. Le correctif devrait être ajouter une méthode remplaçable 'ChangeArea' à un rectangle spécialisé dans Square. Cela ne briserait pas la chaîne de l'héritage, ne rendrait pas explicite ce que l'utilisateur voulait faire et n'aurait pas causé le bogue introduit par votre correctif mentionné (qui aurait été intercepté dans une zone d'activation appropriée).
Roy T.
33
@RoyT .: Pourquoi un rectangle devrait-il savoir comment définir sa surface? C'est une propriété entièrement dérivée de la longueur et de la largeur. Et plus précisément, quelle dimension devrait-il changer - la longueur, la largeur ou les deux?
cHao
32
@Roy T. C'est très agréable de dire que vous auriez résolu le problème différemment, mais il s'agit d'un exemple - bien que simplifié - de situations réelles auxquelles les développeurs sont confrontés quotidiennement lors de la maintenance de produits hérités. Et même si vous avez implémenté cette méthode, cela n'empêchera pas les héritiers de violer le LSP et d'introduire des bogues similaires à celui-ci. C'est pourquoi pratiquement toutes les classes du framework .NET sont scellées.
Stephen
30

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 setWidthdocumenté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'utilisation setWidth, vous n'implémentez pas la spécification de Rectangle setWidth. 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:

  • Renvoie uniquement les valeurs que la superclasse renverrait - autrement dit, le type de retour doit être un sous-type du type de retour de la méthode de la superclasse. Le retour est une co-variante.
  • Acceptez toutes les valeurs que le supertype accepterait. Autrement dit, les types d'argument doivent être des supertypes des types d'argument de la méthode superclass. Les arguments sont contre-variantes.

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 et compareTo(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.

lrn
la source
"Si tous vos objets sont immuables, il n'y a pas de problème" - ceci est apparemment une affirmation hors de propos dans le contexte de cette question, qui mentionne explicitement les setters pour la largeur et la hauteur
gnat
11
J'ai trouvé cela intéressant, même quand c'est "apparemment hors de propos"
Jesvin Jose
14
@gnat Je dirais que c'est pertinent parce que la vraie valeur de la question est de reconnaître lorsqu'il existe une relation de sous-typage valide entre deux types. Cela dépend des opérations déclarées par le super-type. Il est donc utile de signaler que le problème disparaît si les méthodes du mutateur disparaissent.
Doval
1
@gnat également, les setters sont des mutateurs , donc LRN dit essentiellement: "Ne faites pas cela et ce n'est pas un problème." Je suis d’accord avec l’immuabilité pour les types simples, mais vous faites valoir un bon point: pour les objets complexes, le problème n’est pas si simple.
Patrick M
1
Considérez-le ainsi: quel est le comportement garanti par la classe 'Rectangle'? Que vous puissiez changer la largeur et la hauteur INDEPENDANT l'un de l'autre. (ie setWidth et setHeight) méthode. Maintenant, si Square est dérivé de Rectangle, Square doit garantir ce comportement. Puisque square ne peut pas garantir ce comportement, c'est un mauvais héritage. Toutefois, si les méthodes setWidth / setHeight sont supprimées de la classe Rectangle, ce comportement n'existe pas et vous pouvez donc dériver la classe Square de Rectangle.
Nitin Bhide
17

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:

  • Un carré est un genre particulier de losange - c’est un losange à angles carrés.
  • Un losange est un type particulier de parallélogramme - c’est un parallélogramme à côtés égaux.
  • Un rectangle est un type particulier de parallélogramme - c'est un parallélogramme à angles carrés
  • Un rectangle, un carré et un parallélogramme sont des formes spéciales de trapèzes. Ce sont des trapèzes à deux ensembles de côtés parallèles.
  • Tout ce qui précède est un type spécial de quadrilatère
  • Tout ce qui précède sont des types spéciaux de formes planes
  • Etc; Je pourrais continuer pendant un certain temps ici.

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.

Eric Lippert
la source
3
Si les objets de formes sont immuables, un IShapetype peut inclure un cadre de sélection, être dessiné, mis à l'échelle et sérialisé, ainsi qu'un IPolygonsous - type avec une méthode permettant de signaler le nombre de sommets et une méthode permettant de renvoyer un IEnumerable<Point>. On pourrait alors avoir un IQuadrilateralsous - type qui dérive de IPolygon, IRhombuset IRectangledérive de cela, et ISquaredérive de IRhombuset IRectangle. 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.
Supercat
Je suis effectivement en désaccord avec Eric ici (pas assez pour un -1 cependant!). Toutes ces relations sont (éventuellement) pertinentes, comme le mentionne @supercat; C'est juste un problème de YAGNI: vous ne l'implémentez pas avant d'en avoir besoin.
Mark Hurd
Très bonne réponse! Devrait être beaucoup plus élevé.
Andrew.fox
1
@MarkHurd - ce n'est pas un problème de YAGNI: la hiérarchie basée sur l'héritage proposée a la forme de la taxonomie décrite, mais elle ne peut pas être écrite pour garantir les relations qui la définissent. Comment IRhombusgarantit-on que tous les Pointretours de ce qui est Enumerable<Point>défini par IPolygoncorrespondent à des arêtes de longueurs égales? Parce que la IRhombusseule 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.
A. Rager Le
14

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
1
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.
Simon Bergot
6
Les rectangles et les carrés ont plus en commun que cela. Le problème est que l'identité d'un rectangle et l'identité d'un carré sont ses longueurs de côté (et l'angle à chaque intersection). La meilleure solution ici est de faire en sorte que les carrés héritent des rectangles, mais rendent les deux immuables.
Stephen
3
@Stephen D'accord. En fait, les rendre immuables est la chose sensée à faire indépendamment des problèmes de sous-typage. Il n'y a aucune raison de les rendre mutables - il n'est pas plus difficile de construire un nouveau carré ou rectangle que de le muter, alors pourquoi ouvrir cette boîte de Pandore? Désormais, vous n'avez plus à vous soucier des alias / effets secondaires et vous pouvez les utiliser comme clés pour cartographier / dessiner si nécessaire. Certains diront "performance", ce à quoi je dirai "optimisation prématurée" jusqu'à ce qu'ils aient réellement mesuré et prouvé que le point chaud se trouve dans le code de forme.
Doval
Désolé les gars, il était tard et j'étais très fatigué quand j'ai écrit la réponse. Je l'ai réécrit pour dire ce que je voulais vraiment dire, le point crucial étant la mutabilité.
13

Et pourquoi Square serait-il utilisable partout où vous vous attendiez à un rectangle?

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:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

En fait, vous le faites tout le temps (parfois même implicitement) lorsque vous utilisez la POO.

et si nous annulons les méthodes SetWidth et SetHeight pour Square, pourquoi y aurait-il un problème?

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.

Déduplicateur
la source
Dans de nombreuses langues, vous n'avez même pas besoin de la Rect r = s;ligne, vous pouvez simplement doSomethingWith(s)et le runtime utilisera tous les appels spour résoudre toutes les Squareméthodes virtuelles .
Patrick M
1
@PatrickM Vous n'en avez pas besoin dans une langue saine qui comporte des sous-types. J'ai inclus cette ligne pour l'exposition, pour être explicite.
Donc, remplacez setWidthet setHeightpour changer la largeur et la hauteur.
ApproachingDarknessFish
@ValekHalfHeart C'est précisément l'option que je considère.
7
@ValekHalfHeart: C'est précisément la violation du principe de substitution de Liskov qui va vous hanter et vous faire passer plusieurs nuits blanches à essayer de trouver un bogue étrange deux ans plus tard, lorsque vous avez oublié comment le code était censé fonctionner.
Jan Hudec
9

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:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

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:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

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.

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

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.

Le Spooniest
la source
8

Le sous-typage concerne le comportement.

Pour que type Bsoit un sous-type A, il doit prendre en charge toutes les opérations prises en Acharge 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 exemple 3 - 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 exemple 5 / 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 + 1vous 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 sous B- types est de Bse comporter de la même Amaniè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 utilisent Rectangleset supposent que l'implémentation suit la spécification. Ensuite, vous avez introduit un sous-type appelé Squaredont 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.

Doval
la source
6

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?

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.

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

Cormac Mulhall
la source
Si les variables de type Rectangleservent uniquement à représenter des valeurs , il est alors possible qu'une classe Squarehérite de Rectangleson 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.
Supercat
Peut-être, mais alors pourquoi s'embêter en premier lieu. Le problème du rectangle / carré n'est pas d'essayer de comprendre comment faire fonctionner la relation "un carré est un rectangle", mais plutôt de réaliser que la relation n'existe pas réellement dans le contexte dans lequel vous utilisez les objets (comportementalement), et comme un avertissement sur le fait de ne pas imposer des relations non pertinentes sur votre domaine.
Cormac Mulhall
Ou pour le dire autrement: n'essayez pas de plier la cuillère. C'est impossible. Au lieu de cela, essayez seulement de réaliser la vérité, qu'il n'y a pas de cuillère. :-)
Cormac Mulhall
1
Avoir un Squaretype immuable qui hérite d'un Rectnagletype 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 un ReadableMatrixtype [type de base un tableau rectangulaire qui peut être stocké de différentes manières, y compris de manière éparse], et une ComputeDeterminantméthode. Il serait peut-être logique de ne ComputeDeterminanttravailler qu'avec un ReadableSquareMatrixtype dérivé de ReadableMatrixce que je considérerais comme un exemple Squaredérivé de a Rectangle.
Supercat
5

Si un carré est un type de rectangle, pourquoi ne peut-il pas hériter d'un rectangle?

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:

  • définir Square comme classe de base, avec widthparamètre et resize(double factor)redimensionner la largeur par le facteur donné
  • définir la classe Rectangle et la sous-classe de Square, car elle ajoute un autre attribut height, et remplace sa resizefonction, qui appelle super.resizepuis 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.

Marin danubien
la source
+1 Ce n'est pas parce qu'un carré est un type spécial de recteur en mathématiques qu'il en va de même pour OO.
Lovis
1
Un carré est un carré et un rectangle est un rectangle. Les relations entre eux devraient également tenir dans la modélisation, ou vous avez un modèle assez médiocre. Les vrais problèmes sont les suivants: 1) si vous les rendez mutables, vous ne modélisez plus les carrés et les rectangles; 2) en supposant que, juste parce que "est une" relation est valable entre deux types d’objets, vous pouvez en substituer un sans distinction.
Doval
4

Parce que, par LSP, créer une relation d’héritage entre les deux et remplacer setWidthet setHeights’assurer que carré a les deux comme même introduit un comportement déroutant et non intuitif. Disons que nous avons un code:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

Mais si la méthode createRectangleest retournée Square, car c'est possible grâce à l' Squarehéritage de Rectange. 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.

Euphorique
la source
2

Ce que dit LSP, c'est que tout ce qui hérite Rectangledoit être un Rectangle. C'est-à-dire qu'il devrait faire ce que tout le monde Rectanglefait.

La documentation pour Rectangleest probablement écrite pour dire que le comportement d'un Rectanglenommé rest le suivant:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

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 de Rectangle. 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 Rectangletelle manière que cela n’implique pas que le code ci-dessus en imprime 10, auquel cas vous Squarepourriez peut-être être un Rectangle. 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.

Steve Jessop
la source
cela semble simplement répéter les points expliqués dans une réponse postée il y a 5 heures
gnat
@gnat: préféreriez-vous que j'édite cette autre réponse jusqu'à cette brièveté? ;-) Je ne pense pas pouvoir le faire sans supprimer les points que l’autre intervenant estime vraisemblablement nécessaires pour répondre à la question, et je pense que non.
Steve Jessop
1

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/

Warbo
la source