Kann das Kreis-Ellipsen-Problem durch Umkehren der Beziehung gelöst werden?

12

Wenn Sie Circleextend habenEllipse , wird das Liskov-Substitutionsprinzip verletzt , da es eine Nachbedingung ändert: Sie können nämlich X und Y unabhängig voneinander festlegen, um eine Ellipse zu zeichnen, aber X muss für Kreise immer gleich Y sein.

Aber liegt das Problem hier nicht darin, dass Circle der Untertyp einer Ellipse ist? Können wir die Beziehung nicht umkehren?

Circle ist also der Supertyp - es gibt nur eine Methode setRadius.

Dann erweitert Ellipse Circle durch Hinzufügen von setXund setY. Der Aufruf setRadiusauf Ellipse würde sowohl X und Y eingestellt - was bedeutet die Nachbedingung auf setRadius beibehalten wird , aber Sie können nun X und Y unabhängig voneinander durch eine erweiterte Schnittstelle.

HorusKol
quelle
1
Haben Sie sich zuerst mit Wikipedia beschäftigt ( en.wikipedia.org/wiki/Circle-ellipse_problem )?
Doc Brown
1
Ja - ich verlinke es sogar in meiner Frage ...
HorusKol
6
Und genau dieser Punkt wird in diesem Artikel behandelt, daher ist mir nicht klar, was Sie fragen.
Philip Kendall
6
"Einige Autoren haben vorgeschlagen, die Beziehung zwischen Kreis und Ellipse umzukehren, da eine Ellipse ein Kreis mit zusätzlichen Fähigkeiten ist. Leider erfüllen Ellipsen viele der Invarianten von Kreisen nicht. Wenn Kreis einen Methodenradius hat, hat Ellipse jetzt einen es auch zur Verfügung zu stellen. "
Philip Kendall
2
Was ich als die klarste Erklärung dafür fand, warum dieses Problem schlechte Voraussetzungen hat, lag ganz unten im Wikipedia-Artikel: en.wikipedia.org/wiki/… . Je nach Situation gibt es mehrere klare Designs, aber es kommt darauf an, was Sie von diesen beiden Klassen brauchen , um zu tun , nicht zu sein .
Arthur Havlicek

Antworten:

36

Aber liegt das Problem hier nicht darin, dass Circle der Untertyp einer Ellipse ist? Können wir die Beziehung nicht umkehren?

Das Problem mit diesem (und dem Quadrat / Rechteck-Problem) besteht darin, dass fälschlicherweise angenommen wird, dass eine Beziehung in einer Domäne (Geometrie) in einer anderen (Verhalten) gilt.

Ein Kreis und eine Ellipse hängen zusammen, wenn Sie sie durch das Prisma der geometrischen Theorie betrachten. Dies ist jedoch nicht die einzige Domain, die Sie sich ansehen können.

Objektorientiertes Design befasst sich mit Verhalten .

Das bestimmende Merkmal eines Objekts ist das Verhalten, für das das Objekt verantwortlich ist. Und auf dem Gebiet des Verhaltens verhalten sich Kreis und Ellipse so unterschiedlich, dass es wahrscheinlich besser ist, sie überhaupt nicht als verwandt zu betrachten. In diesem Bereich haben eine Ellipse und ein Kreis keine signifikante Beziehung.

Die Lehre hier ist, die Domäne zu wählen, die für OOD am sinnvollsten ist, und nicht zu versuchen, sich in einer Beziehung zu verlieren, nur weil sie in einer anderen Domäne existiert.

Das häufigste Beispiel für diesen Fehler in der realen Welt ist die Annahme, dass Objekte verwandt sind (oder sogar dieselbe Klasse), weil sie ähnlich sind Daten haben, auch wenn ihr Verhalten sehr unterschiedlich ist. Dies ist ein häufiges Problem, wenn Sie anfangen, Objekte "Daten zuerst" zu erstellen, indem Sie definieren, wohin die Daten gehen. Sie können mit einer Klasse enden, die über Daten verknüpft ist, die ein völlig anderes Verhalten aufweisen. Beispielsweise haben sowohl die Gehaltsabrechnung als auch die Mitarbeiterobjekte möglicherweise ein Attribut "Bruttogehalt", aber ein Mitarbeiter ist keine Art von Gehaltsabrechnung und eine Gehaltsabrechnung ist keine Art von Mitarbeiter.

Cormac Mulhall
quelle
Die Trennung der Anliegen der (Anwendungs-) Domäne von den Verhaltens- und Verantwortungsfähigkeiten von OOD ist ein sehr wichtiger Punkt. In einer Zeichenanwendung sollten Sie beispielsweise in der Lage sein, einen Kreis in ein Quadrat zu verwandeln. Dies ist jedoch mit Klassen / Objekten in den meisten Sprachen nicht einfach zu modellieren (da Objekte normalerweise die Klasse nicht ändern können). Daher ist die Anwendungsdomäne nicht immer der Vererbungshierarchie einer bestimmten OOP-Sprache zugeordnet, und wir sollten nicht versuchen, sie zu erzwingen. In vielen Fällen ist die Zusammensetzung besser.
Erik Eidt
3
Diese Antwort ist bei weitem das Beste, was ich über das gesamte Problem gesehen habe, und wie das Potenzial für Designfehler in allgemeineren Fällen entstehen kann. Vielen Dank
HorusKol
1
@ErikEidt Das Problem eines Objektänderungsverhaltens kann in OOD durch Zerlegung gelöst werden. Wenn sich beispielsweise eine morphbare Form in einen Kreis verwandelt, müssen Sie die Klasse nicht ändern. Stattdessen nimmt die Klasse ein aktuelles geometrisches Verhaltensobjekt, das Sie beim Morphen gegen ein anderes Verhalten austauschen können. Diese andere Klasse enthält die Regeln der geometrischen Form, die gerade modelliert wird, und die Klasse für morphbare Formen unterscheidet sich hinsichtlich des geometrischen Verhaltens von dieser Klasse. Wenn sich das Objekt in eine andere Klasse verwandelt, ändern Sie die Verhaltensklasse in etwas anderes.
Cormac Mulhall
2
@Cormac, richtig! Generell würde ich das eine Form der Komposition nennen, obwohl man genauer ein Strategiemuster oder so etwas identifizieren könnte. Im Wesentlichen haben Sie eine Identität, die sich nicht verändert, und andere Dinge, die dann geändert werden können. Alles in allem eine gute Hervorhebung des Unterschieds zwischen Anwendungsdomänenkonzepten und den Einzelheiten des OOP einer bestimmten Sprache sowie der Notwendigkeit, diese zuzuordnen (dh Architektur, Design und Programmierung).
Erik Eidt
1
Aber ein Job kann ein Gehaltsscheck sein.
8

Kreise sind ein Sonderfall von Ellipsen, nämlich dass beide Achsen der Ellipsen gleich sind. In der Problemdomäne (Geometrie) ist es grundsätzlich falsch, zu behaupten, Ellipsen könnten eine Art Kreis sein. Die Verwendung dieses fehlerhaften Modells würde gegen viele Garantien eines Kreises verstoßen, z. B. "Alle Punkte auf dem Kreis haben den gleichen Abstand zum Mittelpunkt". Auch das wäre eine Verletzung des Liskov-Substitutionsprinzips. Wie würde eine Ellipse einen einzigen Radius haben? (Nicht setRadius()aber was noch wichtiger ist getRadius())

Während die Modellierung von Kreisen als Subtyp von Ellipsen nicht grundsätzlich falsch ist, ist es die Einführung der Veränderlichkeit, die dieses Modell zerstört. Ohne die setX()und setY()Methoden gibt es keine LSP Verletzung. Wenn ein Objekt mit unterschiedlichen Dimensionen benötigt wird, ist das Erstellen einer neuen Instanz eine bessere Lösung:

class Ellipse {
  final double x;
  final double y;
  ...
  Ellipse withX(double newX) {
    return new Ellipse(x: newX, y: y);
  }
}
amon
quelle
1
in Ordnung - so, wenn es eine gemeinsame Schnittstelle zwischen war Ellipseund Circle(wie getArea) , die zu einer Art abstrahiert werden würden Shape- könnte Ellipseund Circleseparat Subtyp aus Shapeund satisfy LSP?
HorusKol
1
@ HorusKol Ja. Zwei Klassen, die eine Schnittstelle erben, die beide wirklich korrekt implementieren, sind völlig in Ordnung.
Ixrec
6

Es ist von Anfang an ein Fehler, darauf zu bestehen, eine "Ellipse" - und eine "Circle" -Klasse zu haben, in der eine Unterklasse der anderen ist. Sie haben zwei realistische Möglichkeiten: Eine besteht darin, separate Klassen zu haben. Sie können eine gemeinsame Oberklasse haben, z. B. für Farben, ob das Objekt gefüllt ist, Linienbreite zum Zeichnen usw.

Die andere ist, nur eine Klasse mit dem Namen "Ellipse" zu haben. Wenn Sie diese Klasse haben, ist es einfach genug, sie für die Darstellung von Kreisen zu verwenden (je nach Implementierungsdetails kann es zu Überfüllungen kommen; eine Ellipse hat einen bestimmten Winkel, und die Berechnung dieses Winkels darf für eine kreisförmige Ellipse keine Probleme bereiten). Sie könnten sogar spezielle Methoden für kreisförmige Ellipsen verwenden, aber diese "kreisförmigen Ellipsen" wären immer noch vollständige "Ellipsen" -Objekte.

gnasher729
quelle
Es könnte eine IsCircle-Methode geben, die prüft, ob ein bestimmtes Objekt der Klasse Ellipse tatsächlich beide Achsen gleich hat. Sie haben auch auf das Winkelproblem hingewiesen. Kreise können nicht gedreht werden.
6

Cormac hat eine wirklich gute Antwort, aber ich möchte nur kurz auf den Grund für die Verwirrung eingehen.

Die Vererbung in OO wird oft mit realen Metaphern gelehrt, wie "Äpfel und Orangen sind beide Unterklassen von Früchten". Leider führt dies zu der falschen Annahme, dass Typen in OO gemäß einigen taxonomischen Hierarchien modelliert werden sollten, die unabhängig vom Programm existieren.

Beim Softwaredesign sollten die Typen jedoch gemäß den Anforderungen der Anwendung modelliert werden. Klassifizierungen in anderen Bereichen sind normalerweise irrelevant. In einer tatsächlichen Anwendung mit "Apple" - und "Orange" -Objekten - beispielsweise einem Bestandsverwaltungssystem für einen Supermarkt - werden sie wahrscheinlich überhaupt keine unterschiedlichen Klassen sein, und Kategorien wie "Obst" werden eher Attribute als Supertypen sein.

Das Kreisellipsenproblem ist ein roter Hering. In der Geometrie ist ein Kreis eine Spezialisierung einer Ellipse, aber die Klassen in Ihrem Beispiel sind keine geometrischen Figuren. Entscheidend ist, dass geometrische Figuren nicht veränderbar sind. Sie lassen sich verwandelt , wenn auch, aber dann ein Kreis kann in eine Ellipse verwandelt werden. Ein Modell, bei dem Kreise den Radius ändern können, sich jedoch nicht in eine Ellipse ändern, entspricht also nicht der Geometrie. Ein solches Modell kann in einer bestimmten Anwendung (z. B. einem Zeichenwerkzeug) sinnvoll sein, die geometrische Klassifizierung ist jedoch für die Gestaltung der Klassenhierarchie unerheblich.

Sollte Circle also eine Unterklasse von Ellipse sein oder umgekehrt? Es hängt ganz von den Anforderungen der jeweiligen Anwendung ab, in der diese Objekte verwendet werden. Eine Zeichenanwendung kann verschiedene Möglichkeiten zur Behandlung von Kreisen und Ellipsen haben:

  1. Behandeln Sie Kreise und Ellipsen als unterschiedliche Arten von Formen mit unterschiedlicher Benutzeroberfläche (z. B. zwei Ziehpunkte zur Größenänderung auf einer Ellipse, ein Ziehpunkt auf einem Kreis). Dies bedeutet, dass Sie eine Ellipse haben können, die aus Sicht der Anwendung geometrisch ein Kreis, aber kein Kreis ist.

  2. Behandeln Sie alle Ellipsen einschließlich der Kreise gleich, aber haben Sie die Option, x und y auf den gleichen Wert zu "sperren".

  3. Ellipsen sind nur Kreise, in denen eine Skalierungstransformation angewendet wurde.

Jedes mögliche Design führt zu einem anderen Objektmodell -

Im ersten Fall werden Kreis und Ellipsen sein Geschwister Geschwisterklassen

In der zweiten Klasse wird es überhaupt keine eigene Circle-Klasse geben

In der dritten Klasse wird es keine eindeutige Ellipse-Klasse geben. Das so genannte Kreisellipsenproblem taucht in keinem von diesen auf.

Um die gestellte Frage zu beantworten: Soll der Kreis die Ellipse erweitern? Die Antwort lautet: Es kommt darauf an, was Sie damit machen wollen. Aber wahrscheinlich nicht.

JacquesB
quelle
1
Eine sehr gute Antwort!
Utsav T
3

Nach den LSP-Punkten gibt es eine „richtige“ Lösung für dieses Problem: @HorusKol und @Ixrec - beide Typen wurden von Shape abgeleitet. Aber es hängt von dem Modell ab, von dem aus Sie arbeiten. Sie sollten also immer darauf zurückgreifen.

Was mir beigebracht wurde ist:

Wenn der Subtyp nicht dasselbe Verhalten wie der Supertyp ausführen kann, bleibt die Beziehung in der IS-A-Prämisse nicht erhalten - sie sollte geändert werden.

  • Ein Subtyp ist ein SUPERSET des Supertyps.
  • Ein Supertyp ist ein SUBSET des Subtyps.

Auf Englisch:

  • Ein abgeleiteter Typ ist ein SUPERSET des Basistyps.
  • Ein Basistyp ist ein SUBSET des abgeleiteten Typs.

(Beispiel:

  • Ein Auto mit einem Bad-Boy-Auspuff ist immer noch ein Auto (nach Meinung einiger).
  • Ein Auto ohne Motor, Räder, Zahnstange, Antriebsstrang und nur die übrige Schale ist kein "Auto", sondern nur eine Schale.)

So funktioniert Klassifizierung (dh in der Tierwelt) und im Prinzip in OO.

Wenn Sie dies als Definition von Vererbung und Polymorphismus verwenden (die immer zusammen geschrieben werden), sollten Sie versuchen, die zu modellierenden Typen zu überdenken, wenn dieses Prinzip gebrochen ist.

Wie von @HorusKul und @Ixrec erwähnt, haben Sie in der Mathematik klar definierte Typen. Aber in der Mathematik ist ein Kreis eine Ellipse, weil es eine UNTERmenge der Ellipse ist. In OOP funktioniert die Vererbung jedoch nicht so. Eine Klasse sollte nur erben, wenn es sich um ein SUPERSET (eine Erweiterung) einer vorhandenen Klasse handelt. Dies bedeutet, dass sie in allen Kontexten immer noch die Basisklasse ist.

Auf dieser Grundlage sollte die Lösung meines Erachtens leicht umformuliert werden.

Haben Sie einen Shape-Basistyp, dann RoundedShape (effektiv ein Kreis, aber ich habe hier einen anderen Namen verwendet, DELIBERATELY ...)

... dann Ellipse.

Dieser Weg:

  • RoundedShape ist eine Form.
  • Ellipse ist eine runde Form.

(Dies ist jetzt für Menschen in der Sprache sinnvoll. Wir haben bereits ein klar definiertes Konzept eines „Kreises“ im Kopf, und was wir hier durch Verallgemeinerung (Aggregation) versuchen, bricht dieses Konzept.)

Andy Gut
quelle
Unsere klar definierten Konzepte funktionieren in der Praxis nicht immer.
-1

Aus einer OO-Perspektive erweitert die Ellipse den Kreis. Sie ist darauf spezialisiert, indem sie einige Eigenschaften hinzufügt. Die vorhandenen Eigenschaften des Kreises bleiben in Ellipse, es wird nur komplexer und spezifischer. Ich sehe keine Probleme mit dem Verhalten in diesem Fall, wie es Cormac tut, Formen haben kein Verhalten. Das einzige Problem ist, dass es sich in einem liguistischen oder mathematischen Sinn nicht richtig anfühlt, "eine Ellipse ist ein Kreis" zu sagen. Weil der ganze Sinn der Übung, der nicht erwähnt wird, aber dennoch implizit ist, darin bestand, geometrische Formen zu klassifizieren. Dies mag ein guter Grund sein, Kreis und Ellipse als Peers zu betrachten, sie nicht durch Vererbung zu verknüpfen und zu akzeptieren, dass sie zufällig einige der gleichen Eigenschaften haben, und NICHT zuzulassen, dass Ihr verdrehter OO-Verstand bei dieser Beobachtung seinen Weg findet.

Martin Maat
quelle