Le problème cercle-ellipse peut-il être résolu en inversant la relation?


13

La CircleprolongationEllipse 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 setXet setY. Appeler setRadiussur 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.



1
oui - je l'ai même lié dans ma question ...
HorusKol

6
Et ce point exact est couvert dans cet article, donc je ne suis pas clair sur ce que vous demandez?
Philip Kendall

6
"Certains auteurs ont suggéré d'inverser la relation entre le cercle et l'ellipse, au motif qu'une ellipse est un cercle avec des capacités supplémentaires. Malheureusement, les ellipses ne satisfont pas la plupart des invariants des cercles; si Circle a un rayon de méthode, Ellipse aura maintenant pour le fournir également. "
Philip Kendall

3
Ce que j'ai trouvé être l'explication la plus claire de la raison pour laquelle ce problème a de mauvaises prémisses se trouvait tout en bas de l'article de wikipedia: en.wikipedia.org/wiki/… . Selon la situation, il existe plusieurs designs épurés, mais cela dépend de ce que vous avez besoin de ces deux classes pour faire , pas pour être .
Arthur Havlicek

Réponses:


37

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?

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 séparation des préoccupations du domaine (d'application) et des capacités comportementales et de responsabilité d'OOD est un point très important. Par exemple, dans une application de dessin, vous devriez peut-être pouvoir transformer un cercle en carré, mais cela n'est pas facilement modélisé à l'aide de classes / objets dans la plupart des langues (car les objets ne peuvent généralement pas changer de classe). Ainsi, le domaine d'application ne correspond pas toujours bien à la hiérarchie d'héritage d'un langage OOP donné et nous ne devrions pas essayer de le forcer; dans de nombreux cas, la composition est meilleure.
Erik Eidt

3
Cette réponse est de loin la meilleure chose que j'ai vue sur tout le problème, et comment le potentiel d'erreurs de conception peut survenir dans des cas plus généraux. Merci
HorusKol

1
@ErikEidt Le problème d'un comportement de changement d'objet peut être résolu dans OOD par décomposition. Par exemple, si une forme morphable se transforme en cercle, vous n'avez pas à changer de classe. Au lieu de cela, la classe prend un objet de comportement géométrique actuel que vous pouvez échanger contre un autre comportement lorsque vous transformez. Cette autre classe contient les règles de la forme géométrique en cours de modélisation, et la classe de forme morpable s'en remet à cette classe pour le comportement géométrique. Si l'objet se transforme en une classe différente, vous changez la classe de comportement en quelque chose d'autre.
Cormac Mulhall

2
@Cormac, c'est vrai! De manière générale, j'appellerais cela une forme de composition, comme je l'ai mentionné, bien que vous puissiez identifier, plus spécifiquement, un modèle de stratégie ou quelque chose. En substance, vous avez une identité qui ne se transforme pas et d'autres choses qui peuvent ensuite être modifiées. Dans l'ensemble, une bonne mise en évidence de la différence entre les concepts de domaine d'application et les particularités de la POO d'un langage donné, et la nécessité de les mapper entre eux (c'est-à-dire l'architecture, la conception et la programmation).
Erik Eidt

1
Mais un travail peut être un chèque de paie.

8

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 surtout getRadius())

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()et setY(), 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:

class Ellipse {
  final double x;
  final double y;
  ...
  Ellipse withX(double newX) {
    return new Ellipse(x: newX, y: y);
  }
}

1
ok - donc, s'il y avait une interface commune entre Ellipseet Circle(comme getArea) qui serait abstraite à un type Shape- pourrait Ellipseet Circleséparément sous- taper Shapeet satisfaire LSP?
HorusKol

1
@HorusKol Oui. Deux classes héritant d'une interface qu'elles implémentent vraiment toutes les deux correctement sont tout à fait correctes.
Ixrec

7

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:

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

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

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


1
Une très bonne réponse!
Utsav T

6

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.


Il pourrait y avoir une méthode IsCircle qui vérifierait si un objet particulier de la classe Ellipse a en fait les deux axes identiques. Vous avez également souligné le problème de l'angle. Les cercles ne peuvent pas être «tournés».

3

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.

  • Un sous-type est un SUPERSET du super-type.
  • Un super-type est un SOUS-ENSEMBLE du sous-type.

En anglais:

  • Un type dérivé est un SUPERSET du type de base.
  • Un type de base est un sous-ensemble du type dérivé.

(Exemple:

  • Une voiture avec un échappement bad boy est toujours une voiture (selon certains).
  • Une voiture sans moteur, roues, crémaillère de direction, transmission, et il ne reste que la coque, n'est pas une `` voiture '', c'est juste une coque.)

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:

  • RoundedShape est une forme.
  • Ellipse est une forme arrondie.

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


Nos concepts clairement définis ne fonctionnent pas toujours dans la pratique.

-1

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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.