Avoir - Circle
étendre Ellipse
pause le Principe de la Substition Liskov , car il modifie une postcondition: à savoir, vous pouvez définir x et y indépendamment vers Dessinez une ellipse, mais x doit toujours égaler y pour les cercles.
Mais le problème ici n'est-il pas causé par un cercle être le sous-type d'une ellipse? Ne pouvions-nous pas inverser la relation?
Donc, le cercle est le Superype - il a une seule méthode setRadius
.
Ensuite, Ellipse étend le cercle en ajoutant setX
et setY
. Appeler setRadius
sur Ellipse définirait les x et y - ce qui signifie que la componditionnement sur SETRADIUS est maintenue, mais vous pouvez désormais définir x et y indépendamment via une interface étendue.
Mais le problème ici n'est-il pas causé par un cercle être le sous-type d'une ellipse? Ne pouvions-nous pas inverser la relation?
Le problème avec ceci (et le problème carré/rectangle) suppose faussement une relation dans un domaine (géométrie) dé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 à regarder.
Conception orientée objet traite du comportement .
La caractéristique définissante d'un objet est le comportement que l'objet est responsable de. Et dans le domaine du comportement, un cercle et ellipse ont un comportement aussi différent qu'il est probablement préférable de ne pas penser à eux comme apparentées du tout. Dans ce domaine, une ellipse et un cercle n'ont aucune relation significative.
La leçon ici est de choisir le domaine qui a le plus de sens pour l'OOD, de ne pas essayer de rouler dans une relation simplement parce qu'il existe dans un domaine différent.
L'exemple le plus courant du monde réel de cette erreur est de supposer que des objets sont liés (ou même la même classe) car ils ont similaires données même si leur comportement est très différent. Il s'agit d'un problème courant lorsque vous commencez à construire des objets "Données en premier" en définissant l'endroit où les données vont. Vous pouvez vous retrouver avec une classe liée via des données qui ont un comportement totalement différent. Par exemple, les objets PaySlip et les objets d'employé peuvent avoir un attribut "salaire brut", mais un employé n'est pas un type de compagnie de paie et une lampe de paiement n'est pas un type d'employé.
Les cercles sont un cas particulier d'ellipses, à savoir que les deux axes des ellipsis sont les mêmes. Il est fondamentalement faux dans le domaine problématique (géométrie) d'indiquer que les ellipses pourraient être une sorte de cercle. L'utilisation de ce modèle imparfaite violerait de nombreuses garanties d'un cercle, par exemple "tous les points du cercle ont la même distance du centre". Cela aussi serait une violation principale de substitution de Liskov. Comment une ellipse aurait-elle un seul rayon? (Pas setRadius()
mais plus important encore getRadius()
)
Alors que la modélisation des cercles en tant que sous-type d'ellipses n'est pas fondamentalement fausse, c'est l'introduction de la mutabilité qui brise ce modèle. Sans la fonction setX()
et setY()
Méthodes, il n'y a pas de violation du LSP. S'il est nécessaire d'avoir un objet avec différentes dimensions, la création d'une nouvelle instance est une meilleure solution:
class Ellipse {
final double x;
final double y;
...
Ellipse withX(double newX) {
return new Ellipse(x: newX, y: y);
}
}
Cormac a une très bonne réponse, mais je veux juste élaborer un peu sur la raison de la confusion en premier lieu.
Héritage dans OO est souvent enseigné en utilisant des métaphores du monde réel, comme "les pommes et les oranges sont tous deux des sous-classes de fruits". Malheureusement, cela conduit à la croyance erronée selon laquelle Types In OO doit être modélisé en fonction de certaines hiérarchies taxonomiques existantes indépendantes du programme.
Mais dans la conception logicielle, les types doivent être modélisés en fonction des exigences de la demande. Les classifications dans d'autres domaines ne sont généralement pas pertinentes. Dans une application réelle avec des objets "Apple" et "Orange" - Dites un système de gestion des stocks pour un supermarché - ils ne seront probablement pas des classes distinctes du tout, et des catégories telles que "fruits" seront des attributs plutôt que des super-types.
Le problème Circle-ellipse est un hareng rouge. Dans la 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. Crucialement, les figures géométriques ne sont pas mutables. Ils peuvent être transformés, cependant, mais un cercle peut être transformé en ellipsis. Donc, un modèle où les cercles peuvent changer de rayon mais que cela ne passe pas à une ellipsis ne correspond pas à la géométrie. Un tel modèle peut avoir un sens dans une application particulière (dire un outil de dessin) mais la classification géométrique n'est pas pertinente pour la façon dont vous concevez la hiérarchie de la classe.
Alors, le cercle 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 pourrait avoir des choix différents dans la manière de traiter les cercles et les ellipses:
Traitez les cercles et les ellipses comme des types distincts de formes avec une interface utilisateur différente (par exemple deux personnes de redimensionnement sur une ellipsie, 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 même, mais ont une option pour "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 à différents modèles d'objet -
Dans le 1er cas, le cercle et les ellipses seront frère classes
Dans le 2e, il n'y aura pas de classe de cercle distincte du tout
Dans le 3ème, il n'y aura pas de classe d'ellipse distincte. Donc, le problème dit Circle-ellipse n'entre pas la photo dans aucun de ces éléments.
Donc, pour répondre à la question comme posé: Devrait-il entendre Ellipse? La réponse est la suivante: cela dépend de ce que vous voulez faire avec cela. Mais probablement pas.
C'est une erreur de commencer à insister pour avoir une "ellipse" et une classe "cercle" où l'on est une sous-classe de l'autre. Vous avez deux choix réalistes: l'un est d'avoir des cours séparés. Ils peuvent avoir une superclasse commune, pour des choses comme la couleur, que l'objet soit rempli, la largeur de ligne pour dessiner, etc.
L'autre est d'avoir une classe nommée "Ellipse" seulement. Si vous avez cette classe, il est suffisamment facile de l'utiliser pour représenter des cercles (il peut y avoir des pièges en fonction des détails de la mise en œuvre; une ellipse aura un peu d'angle et le calcul de cet angle ne doit pas avoir de problèmes pour une ellipse en forme de cercle). Vous pourriez même avoir des méthodes spécialisées pour des ellipses circulaires, mais ces "ellipses circulaires" seraient toujours des objets "Ellipse".
Suite aux points LSP, une solution "appropriée" à ce problème est que @horuskol et @ixRec est venu sur les deux types de forme. Mais cela dépend du modèle que vous travaillez, de sorte que vous devriez toujours y retourner.
Ce que j'ai appris est:
Si le sous-type ne peut pas effectuer le même comportement que le type Super-Type, la relation ne tient pas dans la zone IS-A - Il devrait être modifié.
En anglais:
(Exemple:
C'est ainsi que la classification fonctionne (c'est-à-dire dans le monde animal), et en principe, à OO.
En utilisant cela comme définition de l'héritage et du polymorphisme (qui sont toujours écrits ensemble), si ce principe est cassé, vous devriez essayer de repenser les types que vous essayez de modéliser.
Comme mentionné par @horuskul et @ixRec, dans Maths, vous avez des types clairement définis. Mais en maths, un cercle est une ellipse parce que c'est un sous-ensemble d'ellipse. Mais dans OOP Ce n'est pas la façon dont l'héritage fonctionne. Une classe ne doit hériter que s'il s'agit d'une superset (une extension) d'une classe existante - c'est-à-dire qu'elleIS la classe de base dans tous les contextes.
Sur la base de cela, je pense que la solution devrait être légèrement reformulée.
Avoir un type de base de forme, puis arrondi (effectivement un cercle mais j'ai utilisé un nom différent ici délibérément ...)
... alors ellipse.
De cette façon:
(Cela a maintenant un sens aux gens dans la langue. Nous avons déjà un concept clairement défini d'un "cercle" dans nos esprits et ce que nous essayons de faire ici en généralisant (agrégation) enfreint ce concept.)