Si un carré est un type de rectangle, alors pourquoi un carré ne peut-il pas hériter d'un rectangle? Ou pourquoi est-ce une mauvaise conception?
J'ai entendu des gens dire:
Si vous avez fait dériver Square de Rectangle, alors un Square 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 attendez à un rectangle? Il 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é, alors SetWidth et SetHeight n'ont pas de sens car le réglage de l'un changerait l'autre pour le faire correspondre. Dans ce cas, Square échoue au test de substitution Liskov avec Rectangle et l'abstraction d'avoir Square héritant de Rectangle est mauvaise.
Quelqu'un peut-il expliquer les arguments ci-dessus? Encore une fois, si nous surpassons les méthodes SetWidth et SetHeight dans Square, cela ne résoudrait-il pas ce problème?
J'ai également entendu/lu:
Le vrai problème est que nous ne modélisons pas des rectangles, mais plutôt des "rectangles remodelables", c'est-à-dire des rectangles dont la largeur ou la hauteur peuvent être modifiés après la création (et nous considérons toujours qu'il s'agit du même objet). Si nous regardons la classe rectangle de cette manière, il est clair qu'un carré n'est pas un "rectangle remodelable", 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 crois que "redimensionnable" est le terme correct. Les rectangles sont "redimensionnables", tout comme les carrés. Suis-je en train de manquer quelque chose dans l'argument ci-dessus? Un carré peut être redimensionné comme n'importe quel rectangle.
Fondamentalement, nous voulons que les choses se comportent de manière sensée.
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, c'est que je fixe la longueur du rectangle à 1,1 fois ce qu'elle était avant.
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
rectangle.Length = rectangle.Length * 1.1;
}
}
Maintenant, dans ce cas, tous mes rectangles ont maintenant leur longueur augmentée de 10%, ce qui augmentera leur surface de 10%. Malheureusement, quelqu'un m'a en fait passé un mélange de carrés et de rectangles, et lorsque la longueur du rectangle a été modifiée, la largeur aussi.
Mes tests unitaires réussissent car j'ai écrit tous mes tests unitaires pour utiliser une collection de rectangles. J'ai maintenant introduit un bug 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 dans ma méthode, il obtient une très belle augmentation de 21% de la taille. Jim est heureux et personne n'est plus sage.
Jim est promu pour son excellent travail dans une autre division. Alfred rejoint l'entreprise en tant que junior. Dans son premier rapport de bug, Jill de Advertising a rapporté que le passage de carrés à cette méthode entraîne une augmentation de 21% et souhaite que le bug soit corrigé. Alfred voit que les carrés et les rectangles sont utilisés partout dans le code et se rend compte qu'il est impossible de rompre la chaîne d'héritage. Il n'a pas non plus accès au code source de Accounting. Alfred corrige donc le bug 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 de piratage uber et Jill signe que le bug est corrigé.
Le mois prochain, personne n'est payé parce que la comptabilité dépendait de la possibilité de passer des carrés à la méthode IncreaseRectangleSizeByTenPercent
et d'obtenir une augmentation de surface de 21%. Toute l'entreprise passe en mode "priorité 1 de correction de bogues" pour retrouver la source du problème. Ils remontent le problème à la solution d'Alfred. Ils savent qu'ils doivent satisfaire la comptabilité et la publicité. Ils résolvent donc 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 les programmeurs sont confrontés quotidiennement. 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 écriture, date à laquelle la correction de la violation cassera un tas de choses et ne corrigera pas cela mettra en colère votre plus gros client.
Il existe deux façons réalistes de résoudre ce problème.
La première façon est de rendre le 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 voulez un rectangle avec une longueur et une largeur différentes, vous en créez un nouveau. Les carrés peuvent hériter des rectangles avec bonheur.
La deuxième façon consiste à rompre la chaîne d'héritage entre les carrés et les rectangles. Si un carré est défini comme ayant une seule propriété SideLength
et que les rectangles ont une propriété Length
et Width
et qu'il n'y a pas d'héritage, il est impossible de casser accidentellement des choses en attendant un rectangle et obtenir un carré. En termes C #, vous pouvez seal
votre classe rectangle, ce qui garantit que tous les rectangles que vous obtenez sont en fait des rectangles.
Dans ce cas, j'aime bien la manière "objets immuables" de résoudre le problème. L'identité d'un rectangle est sa longueur et sa largeur. Il est logique que lorsque vous voulez changer 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 nouveau Customer
.
Les violations du principe de substitution de Liskov sont courantes dans le monde réel, principalement parce que beaucoup de code est écrit par des personnes incompétentes/sous la pression du temps/ne se soucient pas/font des erreurs. Cela peut et entraîne des problèmes très désagréables. Dans la plupart des cas, vous souhaitez privilégier la composition à l'héritage à la place.
Si tous vos objets sont immuables, il n'y a pas de problème. Chaque carré est également 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 les accesseurs de propriété.
Il existe des modifications que vous pouvez apporter à un rectangle qui conservent tous les invariants de votre classe Rectangle, mais pas tous les invariants carrés - comme changer la largeur ou la hauteur. Soudain, le comportement d'un rectangle n'est pas seulement ses propriétés, c'est aussi ses modifications possibles. Ce n'est pas seulement ce que vous sortez du Rectangle, c'est aussi ce que vous pouvez mettre.
Si votre Rectangle a une méthode setWidth
qui est documentée comme modifiant la largeur et ne modifiant pas la hauteur, alors Square ne peut pas avoir de méthode compatible. Si vous modifiez la largeur et non la hauteur, le résultat n'est plus un carré valide. Si vous avez choisi de modifier la largeur et la hauteur du carré lorsque vous utilisez setWidth
, vous n'implémentez pas la spécification de setWidth
de Rectangle. 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 valablement envoyer à un carré, vous pouvez également l'envoyer à un rectangle.
C'est une question de co-variance vs contre-variance.
Les méthodes d'une sous-classe appropriée, une où les instances peuvent être utilisées dans tous les cas où la superclasse est attendue, nécessitent que chaque méthode:
Donc, revenons à Rectangle et Square: si Square peut être une sous-classe de Rectangle dépend entièrement des méthodes utilisées par 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 co-variant certaines méthodes dans les arguments, comme avoir compareTo(Rectangle)
sur Rectangle et compareTo(Square)
sur Carré, vous aurez un problème en utilisant un Carré comme 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.
Il y a beaucoup de bonnes réponses ici; La réponse de Stephen en particulier illustre bien pourquoi les violations du principe de substitution conduisent à 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 le carré-est-un-type-de-rectangle spécial qui est rarement mentionné, et c'est: pourquoi nous nous arrêtons avec des carrés et des rectangles? Si nous sommes prêts à dire qu'un carré est un type spécial de rectangle, alors nous devrions sûrement aussi être prêts à dire:
Que diable toutes les relations devraient-elles être ici? Les langages basés sur l'héritage de classe comme C # ou Java n'ont pas été conçus pour représenter ce type de relations complexes avec plusieurs types de contraintes. Il vaut mieux éviter simplement la question en n'essayant pas de représenter tout de ces choses comme des classes avec des relations de sous-typage.
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 rectangle.
Mais dans OO design, c'est un problème. Un objet est ce qu'il est, et cela inclut comportements ainsi que l'état. Si je tiens un objet carré, mais quelqu'un d'autre le modifie pour qu'il soit un rectangle, qui viole le contrat du carré sans aucune faute de ma part, ce qui provoque toutes sortes de mauvaises choses.
Le facteur clé ici est mutabilité. Une forme peut-elle changer une fois construite?
Mutable: si les formes peuvent changer une fois construites, un carré ne peut pas 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 longueur égale, mais pas les côtés adjacents. Le carré doit avoir quatre côtés égaux. La modification d'un carré via une interface rectangulaire peut violer le contrat du carré.
Immuable: si les formes ne peuvent pas changer une fois construites, un objet carré doit également toujours respecter le contrat de 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". Puisqu'un nouvel objet est en construction, la place d'origine continue d'adhérer à ses contrats.
Et pourquoi Square serait-il utilisable partout où vous vous attendez à un rectangle?
Parce que cela fait partie de ce que signifie être un sous-type (voir aussi: principe de substitution Liskov). Vous pouvez le faire, vous devez pouvoir le faire:
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 plus implicitement) lorsque vous utilisez la POO.
et si nous surpassions les méthodes SetWidth et SetHeight pour Square, pourquoi y aurait-il un problème?
Parce que vous ne pouvez pas sensiblement remplacer ceux de 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 n'est pas seulement d'être redimensionnable, il est redimensionnable indépendamment dans les deux dimensions.
Ce que vous décrivez va à l'encontre de ce qu'on appelle le Liskov Substitution Principle. 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 échanger dans une instance de n'importe quelle sous-classe de cette classe, sans introduction de bugs.
Le problème Rectangle-Square n'est pas vraiment un très bon moyen d'introduire Liskov. Il essaie d'expliquer un principe général en utilisant un exemple qui est en fait assez subtil, et va à l'encontre de l'un des intuitifs les plus courants définitions dans toutes les mathématiques. Certains l'appellent le problème Ellipse-Circle pour cette raison, mais ce n'est que légèrement mieux dans la mesure où cela va. 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 Parallélogramme le long de ces lignes:
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 un parallélogramme à angles droits. À première vue, cela peut sembler faire de Rectangle un bon candidat pour hériter de Parallelogram, afin que vous puissiez réutiliser tout ce code délicieux. Toutefois:
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 bogues 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 donc cette interface ne fonctionne pas réellement pour Rectangle héritant de Parallelogram. Si j'échange un rectangle dans un code qui attend un parallélogramme et que ce code essaie 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 voulons généralement dire quelque chose d'un peu plus fort que le simple fait d'y écrire une valeur. Nous impliquons un certain degré d'exclusivité: si vous définissez une valeur, à moins de circonstances extraordinaires, elle restera à cette valeur jusqu'à ce que vous la définissiez à nouveau. Il existe de nombreuses utilisations pour les valeurs qui peuvent être écrit mais ne reste pas défini, mais il existe également de nombreux cas qui dépendent d'une valeur qui reste où elle se trouve une fois que vous l'avez définie. 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 bogues de Rectangle, mais elle en a de nouveaux. Le problème avec setSideA et setSideB est qu'aucun de ceux-ci n'est vraiment définissable: vous pouvez toujours écrire une valeur dans l'un ou l'autre, mais cela changera sous vous si l'autre est écrit. Si j'échange ceci pour un parallélogramme dans le 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 le problème, et c'est pourquoi il y a un problème avec l'utilisation de Rectangle-Square comme introduction à Liskov. Rectangle-Square dépend de la différence entre pouvoir écrire sur quelque chose et pouvoir définir , et c'est une différence beaucoup plus subtile que de pouvoir définir quelque chose par rapport à ce qu'il soit en lecture seule. Rectangle-Square a toujours de la valeur à titre d'exemple, car il documente un gotcha assez commun qui doit être surveillé, mais il ne doit pas être utilisé comme introduction exemple. Laissez d'abord l'apprenant se familiariser avec les bases, puis puis lancez quelque chose de plus dur.
Le sous-typage concerne le comportement.
Pour que le type B
soit un sous-type de type A
, il doit prendre en charge toutes les opérations que le type A
prend en charge avec la même sémantique (fantaisie pour "comportement"). En utilisant la logique selon laquelle chaque B est un A ne fonctionne pas - comportement la compatibilité a le dernier mot. La plupart du temps, "B est une sorte de A" chevauche avec "B se comporte comme A", mais pas toujours .
n exemple:
Considérez l'ensemble des nombres réels. Dans n'importe quelle langue, nous pouvons nous attendre à ce qu'ils prennent en charge les opérations +
, -
, *
, et /
. Considérons maintenant l'ensemble des entiers positifs ({1, 2, 3, ...}). De toute évidence, chaque entier positif est également un nombre réel. Mais le type d'entiers positifs est-il un sous-type du type de nombres réels? Regardons les quatre opérations et voyons si les entiers positifs se comportent de la même manière que les nombres réels:
+
: On peut ajouter des entiers positifs sans problème.-
: Toutes les soustractions d'entiers positifs ne donnent pas des entiers positifs. Par exemple. 3 - 5
.*
: On peut multiplier des entiers positifs sans problème./
: Nous ne pouvons pas toujours diviser des 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 fait pour les entiers de taille finie. Clairement, chaque entier 32 bits est également un entier 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 signifie presque toujours à tort ).
Bien sûr, vous pouvez définir +
pour les entiers 32 bits afin que le résultat soit un entier 64 bits, mais maintenant vous devrez réserver 64 bits d'espace chaque fois que vous ajoutez deux nombres 32 bits. Cela peut être acceptable ou non selon 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 certains types A
, la seule façon de garantir que le programme continuera d'être correct pour certains sous-types B
est si B
se comporte comme A
dans tous les sens.
Vous avez donc le type de Rectangles
, dont la spécification indique que ses côtés peuvent être modifiés indépendamment. Vous avez écrit certains programmes qui utilisent Rectangles
et supposez que l'implémentation suit la spécification. Vous avez ensuite introduit un sous-type appelé Square
dont les côtés ne peuvent pas être redimensionnés indépendamment. Par conséquent, la plupart des programmes qui redimensionnent des rectangles auront désormais tort.
Si un carré est un type de rectangle, pourquoi ne pouvez-vous pas hériter d'un carré d'un rectangle? Ou pourquoi est-ce une mauvaise conception?
Tout d'abord, demandez-vous pourquoi vous pensez qu'un carré est un rectangle.
Bien sûr, la plupart des gens ont appris cela à l'école primaire, et cela semble é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. Un carré n'est-il donc pas un rectangle?
Le fait est que tout dépend de quels sont vos critères initiaux pour regrouper des objets, dans quel contexte regardez-vous ces objets. En 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 d'abord vous demander, est-ce basé sur des critères qui me tiennent à cœur.
Dans la grande majorité des cas, ce ne sera pas du tout ce dont vous vous souciez. La majorité des systèmes qui modélisent des formes, comme 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 c'est le 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 à 90 degrés?
Vous ne modélisez pas un système statique, vous modélisez un système dynamique où des choses vont se produire (des formes vont être créées, détruites, modifiées, dessinées, etc.). Dans ce contexte, vous vous souciez du comportement partagé entre les objets, car votre principale préoccupation est ce que vous pouvez faire avec une forme, quelles règles doivent être maintenues pour avoir toujours un système cohérent.
Dans ce contexte, un carré n'est certainement pas un rectangle, car les règles qui régissent la façon dont le carré peut être modifié ne sont pas les mêmes que le rectangle. Ce ne sont donc pas les mêmes choses.
Dans ce cas, ne les modélisez pas comme tels. Pourquoi voudrais-tu? Il ne vous rapporte rien d'autre qu'une restriction inutile.
Il 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 dirait qu'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 sont similaires que dans un contexte qui ne vous intéresse pas?
Cela met en évidence un problème important lorsque les développeurs arrivent sur 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. Il y a des milliers d'années, les Grecs se préoccupaient des propriétés communes des lignes et des anges des formes et les regroupaient en fonction de celles-ci. Cela ne signifie pas que vous êtes obligé de poursuivre ce regroupement si ce n'est pas ce qui vous intéresse (ce qui dans 99% du temps la modélisation dans les logiciels ne vous intéresse pas).
Beaucoup de réponses à cette question se concentrent sur le sous-typage concernant le regroupement des comportements 'car leur cause les règles .
Mais il est si important de comprendre que vous ne faites pas cela uniquement pour suivre les règles. Vous le faites parce que dans la grande majorité des cas, c'est aussi ce dont vous vous souciez réellement. Peu importe si un carré et un rectangle partagent les mêmes anges internes. Vous vous souciez de ce qu'ils peuvent faire tout en étant des carrés et des rectangles. Vous vous souciez du comportement des objets parce que vous modélisez un système qui se concentre sur la modification du système en fonction des règles de comportement des objets.
Si un carré est un type de rectangle, pourquoi ne pouvez-vous pas hériter d'un carré d'un rectangle?
Le problème consiste à penser que si les choses sont liées d'une manière ou d'une autre dans la réalité, elles doivent être liées exactement de la même manière après la modélisation.
La chose la plus importante 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 ce que vous prévoyez d'utiliser pour ces classes, il est difficile de deviner quelle conception vous devez faire. Mais si vous voulez vraiment avoir seulement la hauteur, la largeur et le redimensionnement, il serait plus logique de:
width
paramètre et resize(double factor)
redimensionner la largeur par le facteur donnéheight
, et remplace sa fonction resize
, qui appelle super.resize
puis redimensionne la hauteur par le facteur donnéDu point de vue de la programmation, il n'y a rien dans Square, que Rectangle n'a pas. Il n'y a aucun sens à faire un carré comme sous-classe de Rectangle.
Parce que par LSP, la création d'une relation d'héritage entre les deux et la substitution de setWidth
et setHeight
pour garantir que square a à la fois le 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 createRectangle
a retourné Square
, car c'est possible grâce à Square
héritant de Rectange
. Ensuite, les attentes sont brisées. Ici, avec ce code, nous nous attendons à ce que la définition de la largeur ou de la hauteur entraîne uniquement un changement de largeur ou de hauteur respectivement. Le point de OOP est que lorsque vous travaillez avec la superclasse, vous n'avez aucune connaissance de toute sous-classe en dessous. Et si la sous-classe change le comportement de sorte qu'il va à l'encontre des attentes que nous avons à propos de la superclasse, alors il y a de fortes chances que des bogues se produisent. Et ce genre de bogues est à la fois difficile à déboguer et à corriger.
L'une des principales idées sur OOP est que c'est le comportement, pas les données qui sont héritées (qui est également l'une des principales idées fausses de l'OMI). Et si vous regardez le carré et le rectangle, ils ont aucun comportement eux-mêmes que nous pourrions relier en relation d'héritage.
Ce que LSP dit, c'est que tout ce qui hérite de Rectangle
doit être un Rectangle
. Autrement dit, il devrait faire tout ce que fait un Rectangle
.
La documentation de Rectangle
est probablement écrite pour dire que le comportement d'un Rectangle
nommé r
est 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
. LSP dit donc 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 "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
de telle manière que cela n'implique pas que le code ci-dessus affiche 10, auquel cas peut-être votre Square
pourrait être un Rectangle
. Vous pourriez voir une documentation qui dit quelque chose comme, "cela fait X. De plus, l'implémentation dans cette classe fait Y". Si c'est le cas, vous avez de bonnes raisons d'extraire une interface de la classe et de faire la distinction entre ce que garantit l'interface 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 en effet partie de la définition raisonnable d'un rectangle mutable.
Les sous-types et, par extension, OO programmation, s'appuient souvent sur le principe de substitution Liskov, que toute valeur de type A peut être utilisée lorsqu'un B est requis, si A <= B. C'est assez beaucoup un axiome dans OO architecture, ie. on suppose que toutes les sous-classes auront cette propriété (et 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 impossible à satisfaire (dans des cas non triviaux)! Ce problème, connu sous le nom de problème de rectangle carré ou de cercle-ellipse ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ) est un exemple célèbre de la difficulté de remplir.
Notez que nous pourrions implémenter de plus en plus de carrés et de rectangles d'observation équivalente, mais seulement en jetant de plus en plus de fonctionnalités jusqu'à ce que la distinction soit inutile.
À titre d'exemple, voir http://okmij.org/ftp/Computation/Subtyping/