Wenn Sie Circle
extend 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 setX
und setY
. Der Aufruf setRadius
auf 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.
object-oriented
solid
liskov-substitution
HorusKol
quelle
quelle
Antworten:
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.
quelle
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 istgetRadius()
)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()
undsetY()
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:quelle
Ellipse
undCircle
(wiegetArea
) , die zu einer Art abstrahiert werden würdenShape
- könnteEllipse
undCircle
separat Subtyp ausShape
und satisfy LSP?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.
quelle
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:
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.
Behandeln Sie alle Ellipsen einschließlich der Kreise gleich, aber haben Sie die Option, x und y auf den gleichen Wert zu "sperren".
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.
quelle
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.
Auf Englisch:
(Beispiel:
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:
(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.)
quelle
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.
quelle