Nous sommes donc probablement familiarisés avec l'exemple fourni dans la plupart des manuels du problème de substitution de Liskov impliquant un carré héritant du rectangle . L'objection à cette approche est que, tandis qu'un carré "est un" rectangle dans un sens mathématique, la classe mère, le rectangle, ne peut pas être facilement décrit en termes de carré et donc ce modèle viole l'une des parties fondamentales de SOLID Programmation - Le principe de substitution de Liskov.
Ma question est de dire qu'il est logique que nous inverser la dépendance de carré "est un" rectangle au rectangle "est un" carré. Évidemment, dans un cadre mathématique c'est faux. Mais un pourcentage important de la raison pour laquelle nous utilisons OOP est de réduire la quantité de, semi unique, code qui doit être écrit ou copier collé, d'un endroit à l'autre. Cela semble être une relation entre Ces deux structures ont du sens, mais clairement pas la relation entre l'enfant parent que nous voyons dans la déclaration classique du problème.
Je viens de terminer un cours sur l'ingénierie logicielle et je ne pense pas que ce point était clairement expliqué. Je sais que juste parce qu'il y a une relation "est une" relation entre deux classes n'implique pas nécessairement qu'il devrait y avoir une abstraction du tout, mais il semble satisfaisant dans un sens mathématique pour y avoir une connexion entre les deux classes.
Notez également que je travaille sur, comme projet latéral, un petit moteur de jeu et une sorte d'abstraction entre eux pourraient avoir un sens.
L'exemple classique de carré ne pouvant pas remplacer le rectangle sans violation de la LSP est un peu une "question d'astuce" et sophistique.
Le problème se pose à cause d'une confusion ... c'est-à-dire un mise en œuvre d'un rectangle n'est pas vraiment un rectangle.
Ayant une largeur et une hauteur réglantes de manière indépendante n'est pas une propriété inhérente d'un rectangle. Un rectangle reste un rectangle même si sa largeur et sa hauteur sont immuables.
Par conséquent, pour un rectangle donné, il est raisonnable que l'on ne devait rien assumer quoi que ce soit sur les contraintes (ou le manque de contraintes) sur ses dimensions. Un rectangle mathématique est juste que, rien de moins, rien de plus, et une place mathématique IS En fait un exemple parfait parfait d'un rectangle mathématique.
Toutefois Un rectangle garantit d'une largeur et d'une hauteur réglables indépendamment (comme on cédait normalement le comportement afin de rendre la classe utile) n'est pas en fait un rectangle ... c'est autre chose. C'est autre chose. C'est un rectangle plus certains comportements garantis supplémentaires.
Par conséquent, cet exemple se pose parce qu'il crée de nouvelles choses, qui ne sont ni des rectangles ni des carrés, mais mal labels eux "rectangles" et "carrés" puis exclaze "whoa! Voici Un cas étrange de sous-ensemble parfait violant le LSP! ". Eh bien que ce n'est pas du tout le cas. Les nouvelles choses ont été créées qui ne sont ni des rectangles ni des carrés.
Pour répondre directement à la question de l'OP, non, cela n'a aucun sens ni mathématiquement ni en informatique de considérer un rectangle comme une sorte de carré. Je ne peux pas voir une bonne raison pour laquelle on hériterait de rectangle de la place. Créer des méthodes dans cette architecture supporterait rapidement peu de ressemblance/cartographie naturelle à ce que ces mots nous entendent normalement.
Le principe de substitution de Liskov indique que si un module de programme utilise une classe de base, la référence à la classe de base peut être remplacée par une classe dérivée sans affecter la fonctionnalité du module de programme.
Par conséquent:
Square
ne peut pas hériter de Rectangle
.
Imaginez la fonction suivante, copiée sans vergogne de Robert C. Martin's Développement logiciel Agile, page 115, le vrai problème:
void g(Rectangle& r)
{
r.SetWidth(5);
r.SetHeight(4);
assert(r.Area() == 20);
}
On peut trouver utile de citer le livre quelques paragraphes ci-dessous:
On peut soutenir que le problème repose sur la fonction
g
- que l'auteur n'avait pas le droit de faire l'hypothèse que la largeur et la hauteur étaient indépendantes. L'auteur deg
serait en désaccord. La fonctiong
prend unRectangle
comme argument. Il existe des invariants, des déclarations de vérité, qui s'appliquent évidemment à une classe nomméeRectangle
, et l'un de ces invariants est que la hauteur et la largeur sont indépendantes. L'auteur deg
avait tout le droit d'affirmer cet invariant. C'est l'auteur deSquare
qui a violé l'invariant.
En effet, que se passe-t-il, c'est que nous pouvions imaginer un tas de contrats associés à la classe rectangle. Ces contrats peuvent être écrits en code, tels que C # ou Java, ou faire partie de l'interface publique, telle que dans Eiffel ou Spec #, ou être supposée ou écrite sous une forme de commentaires.
L'un des contrats est suggéré par Robert C. Martin et se compose de la postcondition de Rectangle::SetWidth(double w)
:
assert((itsWidth == w) && (itsHeight == old.itsHeight));
Square
viole cette postconition, car itsHeight == old.itsHeight
retournerait false
.
Rectangle
ne peut pas hériter de Square
.
Ici, la même logique s'applique. Imaginez que Rectangle
hérite maintenant de Square
. La fonction g(Square& s)
modifie la largeur du carré, par exemple en le multipliant par deux. Compte tenu de la définition de la place, il est sage de supposer que la surface sera multipliée par quatre.
Cependant, en passant une instance de la classe Rectangle
à g
enfreindra l'hypothèse, car la nouvelle surface sera doublée au lieu de quadruple. La même logique s'applique ici et signifie que héritier Rectangle
de Square
est tout aussi mauvais.
Rectangle
ne doit pas hériter de Square
. Un carré est une forme spéciale d'un rectangle (où tous les côtés ont une longueur égale). Donc, la forme générale est Rectangle
et Square
est une spécialisation. Donc, vous pouvez dessiner une généralisation de Square
à Rectangle
, mais pas vice versa.
En ce qui concerne la substitution de Liskov: si vous introduisez des contraintes, vous entrez autour des pièges. Certainement, il est tentant de "hériter simplement", mais bien sûr, vous avez toujours votre cerveau. Et cela a révélé le problème derrière l'héritage du cercle/ellipsis. Donc, vous devez être conscient de ce que vous faites et de prendre soin de la non-évidente. Et: la programmation dogmatique n'est pas nécessairement bonne programmation.
Depuis height/width
sont protégés que vous avez besoin de méthodes de réglage pour les modifier. Et la contrainte sur Square
garantira que la hauteur et la largeur doivent être égales. Maintenant, il peut y avoir des implémentations différentes pour Square
. Soit un setHeight
va d'effet secondaire modifier la largeur (ce qui ne semble pas être une bonne idée). Ou vous soulevez une exception lorsque vous essayez de le faire, ce qui peut être mis en œuvre dans les dérogations. En tant que commodité, vous pouvez ajouter un setSize
avec juste un seul Int pour la hauteur/la largeur qui n'est disponible que dans Square
. Vous pouvez étendre cette conception de la mesure similaire à diverses méthodes.
L'héritage ne s'applique pas du tout, de toute façon. Bien que chaque carré soit un rectangle, cela n'ajoute rien au rectangle. Il s'agit simplement d'une période rectangle, il n'exprime pas le rectangle. L'héritage est inutile à moins que le descendant ne soit (attendu) pour étendre la classe de base en quelque sorte.
La confusion provient du label humain "carré" qui est totalement arbitraire. Si les gens appelleraient un rectangle avec des côtés longs deux fois la longueur des côtés courts A Doubie, et le mot serait dans le dictionnaire anglais, vous verriez la même question sur les douceurs et les rectangles sur Stackexchange.
Il en va de même pour les cercles et les ellipses. Ellipse est le type, cercle est juste une incarnation commune de celui-ci.
Comme vous l'avez souligné, ce n'est pas une bonne idée de Square
hériter d'une modification Rectangle
(tout nouveau code dans Rectangle
pourrait créer Square
s ne sont pas, eh bien, des carrés), ni une bonne idée de Rectangle
hériter de Square
(le rectangle général est non un carré, c'est donc un carré violation claire de la règle de substituabilité).
Le problème doit être abordé d'une autre manière. Les options sont:
Faire à la fois Square
et Rectangle
immuable. Si le constructeur est le seul point où les longueurs peuvent être réglées, il n'ya aucun danger d'un Square
devenant soudainement un non carré à travers l'utilisation de méthodes de Rectangle
. En outre, cela aide à mettre en œuvre une sémantique de valeur stricte, qui rend le code beaucoup plus lisible.
Oubliez environ Square
et utilisez simplement un Rectangle
qui se trouve avoir la même largeur et la même hauteur. Peut être combiné avec une immuabilité comme ci-dessus et vous voudrez peut-être ajouter un constructeur qui ne prend qu'un argument de taille unique pour construire un Rectangle
qui se trouve être un carré.
Si vous avez besoin de transformations affinées quand même, vous pouvez également aller exactement la route opposée: avoir seulement un carré qui occupe le rectangle entre (0,0)
et (1,1)
, et ajoutez une transformation affine sur chaque carré de ce type pour la traduire, étiré-le, faites-la pivoter, cisaillez-la à la forme précise du parallélogramme que vous souhaitez. Bien sûr, vous aurez un constructeur pour les carrés alignés sur l'axe et un pour les rectangles alignés sur l'axe, mais à partir de ce point, vous avez près de la liberté absolue. Bien sûr, il s'agit d'une solution plutôt lourd, mais assez bonne dans certains contextes.
Quelle solution fonctionne le mieux pour vous dépend entièrement de votre cas d'utilisation. Mais j'ai découvert que ce n'est généralement pas une idée brillante de forcer quelque chose dans une relation d'héritage qui ne veut pas y être. Square
et Rectangle
sont une telle paire. Au lieu de cela, recherchez des solutions qui évitent le problème en premier lieu. C'est-à-dire qu'est-ce qui fait un bon développeur de logiciels: la possibilité de penser à la boîte et de proposer des solutions créatives et simples qui correspondent facilement au problème actuel.