La Circle
prolongationEllipse
rompt le principe de la sous-position de Liskov , car elle modifie une postcondition: à savoir, vous pouvez définir X et Y indépendamment pour dessiner une ellipse, mais X doit toujours être égal à Y pour les cercles.
Mais le problème ici n'est-il pas causé par le fait que Circle soit le sous-type d'une ellipse? Ne pourrions-nous pas inverser la relation?
Donc, Circle est le supertype - il a une seule méthode setRadius
.
Ensuite, Ellipse étend Circle en ajoutant setX
et setY
. Appeler setRadius
sur Ellipse définirait à la fois X et Y - ce qui signifie que la postcondition sur setRadius est maintenue, mais vous pouvez maintenant définir X et Y indépendamment via une interface étendue.
object-oriented
solid
liskov-substitution
HorusKol
la source
la source
Réponses:
Le problème avec cela (et le problème du carré / rectangle) est de supposer à tort qu'une relation dans un domaine (géométrie) tient dans un autre (comportement)
Un cercle et une ellipse sont liés si vous les regardez à travers le prisme de la théorie géométrique. Mais ce n'est pas le seul domaine que vous pouvez regarder.
La conception orientée objet traite du comportement .
La caractéristique qui définit un objet est le comportement dont il est responsable. Et dans le domaine du comportement, un cercle et une ellipse ont un comportement si différent qu'il vaut probablement mieux ne pas les considérer comme liés du tout. Dans ce domaine, une ellipse et un cercle n'ont pas de relation significative.
La leçon ici est de choisir le domaine qui a le plus de sens pour OOD, de ne pas essayer de chausse-pied dans une relation simplement parce qu'il existe dans un domaine différent.
L'exemple le plus courant dans le monde réel de cette erreur est de supposer que les objets sont liés (ou même la même classe) car ils ont des données similaires même si leur comportement est très différent. Il s'agit d'un problème courant lorsque vous commencez à créer des objets «données d'abord» en définissant où vont les données. Vous pouvez vous retrouver avec une classe qui est liée via des données qui ont un comportement complètement différent. Par exemple, les fiches de paie et les objets employé peuvent avoir un attribut "salaire brut", mais un employé n'est pas un type de fiche de paie et une fiche de paie n'est pas un type d'employé.
la source
Les cercles sont un cas particulier des ellipses, à savoir que les deux axes des ellipses sont identiques. Il est fondamentalement faux dans le domaine du problème (géométrie) d'affirmer que les ellipses peuvent être une sorte de cercle. L'utilisation de ce modèle défectueux violerait de nombreuses garanties d'un cercle, par exemple «tous les points du cercle ont la même distance au centre». Cela constituerait également une violation du principe de substitution de Liskov. Comment une ellipse aurait-elle un seul rayon? (Pas
setRadius()
mais surtoutgetRadius()
)Si la modélisation des cercles en tant que sous-type d'ellipses n'est pas fondamentalement erronée, c'est l'introduction de la mutabilité qui rompt ce modèle. Sans les méthodes
setX()
etsetY()
, il n'y a pas de violation LSP. S'il est nécessaire d'avoir un objet de différentes dimensions, la création d'une nouvelle instance est une meilleure solution:la source
Ellipse
etCircle
(commegetArea
) qui serait abstraite à un typeShape
- pourraitEllipse
etCircle
séparément sous- taperShape
et satisfaire LSP?Cormac a une très bonne réponse, mais je veux juste développer un peu sur la raison de la confusion en premier lieu.
L'héritage en OO est souvent enseigné en utilisant des métaphores du monde réel, comme «les pommes et les oranges sont toutes deux des sous-classes de fruits». Malheureusement, cela conduit à croire à tort que les types en OO devraient être modélisés selon certaines hiérarchies taxonomiques existant indépendamment du programme.
Mais dans la conception de logiciels, les types doivent être modélisés en fonction des exigences de l'application. Les classifications dans d'autres domaines ne sont généralement pas pertinentes. Dans une application réelle avec des objets "Apple" et "Orange" - par exemple un système de gestion des stocks pour un supermarché - ils ne seront probablement pas du tout des classes distinctes, et des catégories comme "Fruit" seront des attributs plutôt que des supertypes.
Le problème du cercle-ellipse est un hareng rouge. En géométrie, un cercle est une spécialisation d'une ellipse, mais les classes de votre exemple ne sont pas des figures géométriques. Surtout, les figures géométriques ne sont pas modifiables. Ils peuvent être transformés , mais un cercle peut ensuite être transformé en ellipses. Ainsi, un modèle où les cercles peuvent changer de rayon mais pas se transformer en ellipses ne correspond pas à la géométrie. Un tel modèle peut avoir un sens dans une application particulière (par exemple, un outil de dessin), mais la classification géométrique n'a pas d'importance pour la façon dont vous concevez la hiérarchie des classes.
Donc, Circle devrait-il être une sous-classe d'Ellipse ou vice versa? Cela dépend totalement des exigences de l'application particulière qui utilise ces objets. Une application de dessin peut avoir différents choix pour traiter les cercles et les ellipses:
Traitez les cercles et les ellipses comme des types de formes distincts avec une interface utilisateur différente (par exemple, deux poignées de redimensionnement sur une ellipse, une poignée sur un cercle). Cela signifie que vous pouvez avoir une ellipse qui est géométriquement un cercle mais pas un cercle du point de vue de l'application.
Traitez toutes les ellipses, y compris les cercles, de la même manière, mais vous avez la possibilité de "verrouiller" x et y à la même valeur.
Les ellipses ne sont que des cercles où une transformation d'échelle a été appliquée.
Chaque conception possible conduira à un modèle d'objet différent -
Dans le premier cas, Circle et Ellipses seront des classes de frères
Dans le 2ème, il n'y aura pas du tout de classe Cercle distincte
Dans le 3ème, il n'y aura pas de classe Ellipse distincte. Ainsi, le soi-disant problème de cercle-ellipse n'entre dans l'image dans aucun d'entre eux.
Donc, pour répondre à la question posée: le cercle doit-il étendre l'ellipse? La réponse est: cela dépend de ce que vous voulez en faire. Mais probablement pas.
la source
C'est une erreur dès le départ d'insister pour avoir une classe "Ellipse" et une classe "Circle" où l'une est une sous-classe de l'autre. Vous avez deux choix réalistes: l'un consiste à avoir des classes séparées. Ils peuvent avoir une superclasse commune, pour des choses comme la couleur, si l'objet est rempli, la largeur de ligne pour le dessin, etc.
L'autre est d'avoir une seule classe nommée "Ellipse". Si vous avez cette classe, il est assez facile de l'utiliser pour représenter des cercles (il peut y avoir des pièges en fonction des détails d'implémentation; une ellipse aura un certain angle et le calcul de cet angle ne doit pas poser de problème pour une ellipse en forme de cercle). Vous pourriez même avoir des méthodes spécialisées pour les ellipses circulaires, mais ces "ellipses circulaires" seraient toujours des objets "Ellipse" complets.
la source
Après les points LSP, une solution «appropriée» à ce problème est que @HorusKol et @Ixrec sont venus - dérivant les deux types de Shape. Mais cela dépend du modèle à partir duquel vous travaillez, vous devez donc toujours y revenir.
Ce que j'ai appris, c'est:
Si le sous-type ne peut pas exécuter le même comportement que le super-type, la relation ne tient pas dans la prémisse IS-A - elle doit être modifiée.
En anglais:
(Exemple:
C'est ainsi que fonctionne la classification (c'est-à-dire dans le monde animal), et en principe, en OO.
En utilisant cela comme définition de l'héritage et du polymorphisme (qui sont toujours écrits ensemble), si ce principe est brisé, vous devriez essayer de repenser les types que vous essayez de modéliser.
Comme mentionné par @HorusKul et @Ixrec, en mathématiques, vous avez des types clairement définis. Mais en mathématiques, un cercle est une ellipse car c'est un SOUS-ENSEMBLE d'ellipse. Mais dans la POO, ce n'est pas ainsi que fonctionne l'héritage. Une classe ne doit hériter que si elle est un SUPERSET (une extension) d'une classe existante - ce qui signifie qu'elle EST toujours la classe de base dans tous les contextes.
Sur cette base, je pense que la solution devrait être légèrement reformulée.
Avoir un type de base Shape, puis RoundedShape (effectivement un cercle mais j'ai utilisé un nom différent ici DÉLIBÉRÉMENT ...)
... puis Ellipse.
De cette façon:
(Cela a maintenant un sens pour les gens dans le langage. Nous avons déjà un concept clairement défini de `` cercle '' dans nos esprits, et ce que nous essayons de faire ici en généralisant (agrégation) rompt ce concept.)
la source
D'un point de vue OO, l'ellipse étend le cercle, elle se spécialise en ajoutant des propriétés. Les propriétés existantes du cercle tiennent toujours dans l'ellipse, il devient juste plus complexe et plus spécifique. Je ne vois aucun problème de comportement dans ce cas comme Cormac, les formes n'ont aucun comportement. Le seul problème est que, dans un sens liguistique ou mathématique, il ne semble pas juste de dire "une ellipse EST un cercle". Car tout l'intérêt de l'exercice, non mentionné mais néanmoins implicite, était de classer les formes géométriques. C'est peut-être une bonne raison de considérer le cercle et l'ellipse comme des pairs, de ne pas les lier par héritage et d'accepter qu'ils aient simplement certaines des mêmes propriétés et de NE PAS laisser votre esprit OO tordu faire son chemin avec cette observation.
la source