Wenn ein Quadrat eine Art Rechteck ist, warum kann ein Quadrat dann nicht von einem Rechteck erben? Oder warum ist es ein schlechtes Design?
Ich habe Leute sagen hören:
Wenn Sie Square von Rectangle ableiten lassen, sollte ein Square überall dort verwendbar sein, wo Sie ein Rechteck erwarten
Was ist das Problem hier? Und warum sollte Square überall dort einsetzbar sein, wo Sie ein Rechteck erwarten? Es wäre nur dann verwendbar, wenn wir das Square-Objekt erstellen und wenn wir die SetWidth- und SetHeight-Methoden für Square überschreiben, warum sollte es dann ein Problem geben?
Wenn Ihre Rectangle-Basisklasse über die Methoden SetWidth und SetHeight verfügt und Ihre Rectangle-Referenz auf ein Quadrat verweist, sind SetWidth und SetHeight nicht sinnvoll, da sich durch das Festlegen der beiden Methoden gegenseitig ändern würden. In diesem Fall besteht Square den Liskov-Substitutionstest mit Rechteck nicht und die Abstraktion, Square von Rechteck erben zu lassen, ist schlecht.
Kann jemand die obigen Argumente erklären? Wenn wir die Methoden SetWidth und SetHeight in Square überschreiben, kann dieses Problem dann nicht behoben werden?
Ich habe auch gehört / gelesen:
Das eigentliche Problem ist, dass wir keine Rechtecke modellieren, sondern "umformbare Rechtecke", dh Rechtecke, deren Breite oder Höhe nach der Erstellung geändert werden können (und wir betrachten es immer noch als dasselbe Objekt). Wenn wir die Rechteckklasse auf diese Weise betrachten, ist es klar, dass ein Quadrat kein "umformbares Rechteck" ist, da ein Quadrat nicht umgeformt werden kann und (im Allgemeinen) immer noch ein Quadrat ist. Mathematisch sehen wir das Problem nicht, weil die Veränderbarkeit in einem mathematischen Kontext nicht einmal Sinn ergibt
Hier glaube ich, dass "Größenänderung" der richtige Begriff ist. Rechtecke sind "in der Größe veränderbar", ebenso Quadrate. Vermisse ich etwas im obigen Argument? Ein Quadrat kann wie ein Rechteck in der Größe verändert werden.
quelle
Why do we even need Square
? Es ist wie mit zwei Stiften. Ein blauer Stift und ein roter blauer, gelber oder grüner Stift. Der blaue Stift ist überflüssig, zumal er keinen Kostenvorteil bringt.Antworten:
Grundsätzlich wollen wir, dass sich die Dinge vernünftig verhalten.
Betrachten Sie das folgende Problem:
Ich bekomme eine Gruppe von Rechtecken und möchte deren Fläche um 10% vergrößern. Ich stelle also die Länge des Rechtecks auf das 1,1-fache der vorherigen Länge ein.
In diesem Fall wurde die Länge aller Rechtecke um 10% erhöht, wodurch die Fläche um 10% vergrößert wird. Leider hat mir tatsächlich jemand eine Mischung aus Quadraten und Rechtecken übergeben, und als die Länge des Rechtecks geändert wurde, war auch die Breite verändert.
Meine Komponententests bestehen, weil ich alle meine Komponententests geschrieben habe, um eine Sammlung von Rechtecken zu verwenden. Ich habe jetzt einen subtilen Fehler in meine Anwendung eingefügt, der monatelang unbemerkt bleiben kann.
Schlimmer noch, Jim aus der Buchhaltung sieht meine Methode und schreibt einen anderen Code, der die Tatsache ausnutzt, dass er eine sehr schöne Vergrößerung von 21% erhält, wenn er Quadrate in meine Methode übergibt. Jim ist glücklich und niemand ist klüger.
Jim wird für hervorragende Arbeit in eine andere Abteilung befördert. Alfred tritt als Junior in das Unternehmen ein. In seinem ersten Fehlerbericht hat Jill von Advertising berichtet, dass das Übergeben von Quadraten an diese Methode zu einer Steigerung von 21% führt und den Fehler beheben möchte. Alfred erkennt, dass überall im Code Quadrate und Rechtecke verwendet werden, und erkennt, dass es unmöglich ist, die Vererbungskette zu durchbrechen. Er hat auch keinen Zugriff auf den Quellcode des Rechnungswesens. Also behebt Alfred den Fehler folgendermaßen:
Alfred ist mit seinen überragenden Hacking-Fähigkeiten zufrieden und Jill meldet, dass der Fehler behoben ist.
Im nächsten Monat niemand wird bezahlt , weil Accounting abhängig war auf die Möglichkeit, Quadrate an die passieren
IncreaseRectangleSizeByTenPercent
Verfahren und eine Erhöhung der Fläche von 21% bekommen. Das gesamte Unternehmen wechselt in den Bugfix-Modus "Priorität 1", um die Ursache des Problems zu ermitteln. Sie verfolgen das Problem zu Alfred's Verlegenheit. Sie wissen, dass sie sowohl Buchhaltung als auch Werbung glücklich machen müssen. Sie beheben das Problem, indem sie den Benutzer mit dem Methodenaufruf folgendermaßen identifizieren:Und so weiter und so fort.
Diese Anekdote basiert auf realen Situationen, mit denen Programmierer täglich konfrontiert sind. Verstöße gegen das Liskov-Substitutionsprinzip können sehr subtile Fehler hervorrufen, die erst Jahre nach dem Verfassen aufgedeckt werden. Zu diesem Zeitpunkt wird die Behebung des Verstoßes eine Reihe von Problemen lösen und Ihren größten Kunden verärgern , wenn sie nicht behoben werden.
Es gibt zwei realistische Möglichkeiten, dieses Problem zu beheben.
Der erste Weg ist, das Rechteck unveränderlich zu machen. Wenn der Benutzer von Rectangle die Eigenschaften Length und Width nicht ändern kann, wird dieses Problem behoben. Wenn Sie ein Rechteck mit einer anderen Länge und Breite möchten, erstellen Sie ein neues. Quadrate können glücklich von Rechtecken erben.
Die zweite Möglichkeit besteht darin, die Vererbungskette zwischen Quadraten und Rechtecken zu unterbrechen. Wenn ein Quadrat als mit einem einzelnen definiert ist
SideLength
Eigentum und Rechtecken habenLength
undWidth
Eigentum und es gibt keine Vererbung, dann ist es unmöglich, aus Versehen bricht durch ein Rechteck und bekommen einen Platz erwartet. In C # können Sieseal
Ihre Rechteckklasse verwenden, wodurch sichergestellt wird, dass alle Rechtecke, die Sie jemals erhalten, tatsächlich Rechtecke sind.In diesem Fall gefällt mir die Methode "Unveränderliche Objekte", mit der das Problem behoben werden kann. Die Identität eines Rechtecks ist seine Länge und Breite. Wenn Sie die Identität eines Objekts ändern möchten, ist es sinnvoll, dass Sie wirklich ein neues Objekt möchten . Wenn Sie einen alten Kunden verlieren und einen neuen Kunden gewinnen, ändern Sie nicht das
Customer.Id
Feld vom alten Kunden zum neuen Kunden, sondern erstellen einen neuenCustomer
.Verstöße gegen das Liskov-Substitutionsprinzip sind in der realen Welt weit verbreitet, vor allem, weil eine Menge Code von Leuten geschrieben wird, die inkompetent sind / unter Zeitdruck stehen / sich nicht darum kümmern / Fehler machen. Es kann und führt zu einigen sehr schlimmen Problemen. In den meisten Fällen möchten Sie stattdessen die Komposition der Vererbung vorziehen.
quelle
Wenn alle Ihre Objekte unveränderlich sind, gibt es kein Problem. Jedes Quadrat ist auch ein Rechteck. Alle Eigenschaften eines Rechtecks sind auch Eigenschaften eines Quadrats.
Das Problem beginnt, wenn Sie die Möglichkeit hinzufügen, die Objekte zu ändern. Oder wirklich - wenn Sie anfangen, Argumente an das Objekt zu übergeben, statt nur Eigenschafts-Getter zu lesen.
Es gibt Änderungen, die Sie an einem Rechteck vornehmen können, bei denen alle Invarianten Ihrer Rechteckklasse beibehalten werden, aber nicht alle quadratischen Invarianten - wie z. B. das Ändern der Breite oder Höhe. Plötzlich sind das Verhalten eines Rechtecks nicht nur seine Eigenschaften, sondern auch seine möglichen Modifikationen. Es ist nicht nur das, was Sie aus dem Rechteck herausholen, sondern auch das, was Sie hineinstecken können .
Wenn Ihr Rechteck über eine Methode verfügt
setWidth
, bei der dokumentiert ist, dass sie die Breite ändert und die Höhe nicht ändert, kann Square keine kompatible Methode verwenden. Wenn Sie die Breite und nicht die Höhe ändern, ist das Ergebnis kein gültiges Quadrat mehr. Wenn Sie bei der Verwendung sowohl die Breite als auch die Höhe des Quadrats ändernsetWidth
, wird die Spezifikation von Rechtecken nicht implementiertsetWidth
. Sie können einfach nicht gewinnen.Wenn Sie sich ansehen, was Sie in ein Rechteck und ein Quadrat "einfügen" können, welche Nachrichten Sie an sie senden können, werden Sie wahrscheinlich feststellen, dass jede Nachricht, die Sie gültig an ein Quadrat senden können, auch an ein Rechteck gesendet werden kann.
Es geht um Co-Varianz vs. Kontra-Varianz.
Methoden einer richtigen Unterklasse, bei denen Instanzen in allen Fällen verwendet werden können, in denen die Superklasse erwartet wird, erfordern, dass jede Methode:
Zurück zu Rectangle und Square: Ob Square eine Unterklasse von Rectangle sein kann, hängt ganz davon ab, über welche Methoden Rectangle verfügt.
Wenn Rectangle einzelne Setter für Breite und Höhe hat, ist Square keine gute Unterklasse.
Wenn Sie einige Methoden als Co-Variante in den Argumenten
compareTo(Rectangle)
festlegen , z. B. on Rectangle undcompareTo(Square)
on Square, haben Sie ebenfalls Probleme, ein Square als Rectangle zu verwenden.Wenn Sie Ihr Quadrat und Ihr Rechteck so gestalten, dass sie kompatibel sind, wird es wahrscheinlich funktionieren, aber sie sollten zusammen entwickelt werden, oder ich wette, dass es nicht funktioniert.
quelle
Hier gibt es viele gute Antworten; Stephens Antwort macht besonders deutlich, warum Verstöße gegen das Substitutionsprinzip zu realen Konflikten zwischen Teams führen.
Ich dachte, ich könnte kurz auf das spezifische Problem der Rechtecke und Quadrate eingehen, anstatt es als Metapher für andere Verstöße gegen die LSP zu verwenden.
Es gibt ein zusätzliches Problem mit Quadrat ist ein Rechteck der besonderen Art, das selten erwähnt wird, und das ist: Warum hören wir mit Quadraten und Rechtecken auf ? Wenn wir sagen wollen, dass ein Quadrat eine besondere Art von Rechteck ist, dann sollten wir sicherlich auch sagen wollen:
Was in aller Welt sollten alle Beziehungen hier sein? Klassenvererbungsbasierte Sprachen wie C # oder Java wurden nicht entwickelt, um diese Art von komplexen Beziehungen mit mehreren verschiedenen Arten von Einschränkungen darzustellen. Es ist am besten, die Frage ganz zu vermeiden, indem Sie nicht versuchen, all diese Dinge als Klassen mit subtypisierenden Beziehungen darzustellen.
quelle
IShape
Typ handeln, der einen Begrenzungsrahmen enthält, der gezeichnet, skaliert und serialisiert werden kann, und einenIPolygon
Subtyp mit einer Methode zum Melden der Anzahl von Scheitelpunkten und einer Methode zum Zurückgeben vonIEnumerable<Point>
. Man könnte dannIQuadrilateral
Subtyp , die aus leitetIPolygon
,IRhombus
undIRectangle
leiten daraus, undISquare
entstammenIRhombus
undIRectangle
. Mutability würde alles aus dem Fenster werfen, und Mehrfachvererbung funktioniert nicht mit Klassen, aber ich denke, es ist in Ordnung mit unveränderlichen Schnittstellen.IRhombus
sichergestellt werden, dass allePoint
von demEnumerable<Point>
definierten durch zurückgegebenenIPolygon
Kanten gleicher Länge entsprechen? Da die Implementierung derIRhombus
Schnittstelle allein nicht garantiert, dass ein konkretes Objekt eine Raute ist, kann Vererbung nicht die Antwort sein.Aus mathematischer Sicht ist ein Quadrat ein Rechteck. Wenn ein Mathematiker das Quadrat so ändert, dass es nicht mehr dem Quadratkontrakt entspricht, verwandelt es sich in ein Rechteck.
Im OO-Design ist dies jedoch ein Problem. Ein Objekt ist, was es ist, und dies schließt sowohl das Verhalten als auch den Zustand ein. Wenn ich ein quadratisches Objekt halte, es aber von jemand anderem in ein Rechteck geändert wird, verletzt dies unverschuldet den Vertrag des Quadrats. Dies führt dazu, dass alle möglichen schlechten Dinge passieren.
Der Schlüsselfaktor hierbei ist die Wandlungsfähigkeit . Kann sich eine Form ändern, wenn sie einmal konstruiert ist?
Veränderlich: Wenn sich Formen nach der Konstruktion ändern dürfen, kann ein Quadrat keine Is-A-Beziehung zum Rechteck haben. Der Vertrag eines Rechtecks enthält die Bedingung, dass gegenüberliegende Seiten gleich lang sein müssen, benachbarte Seiten jedoch nicht. Das Quadrat muss vier gleiche Seiten haben. Das Ändern eines Quadrats über eine Rechteckschnittstelle kann den Quadratkontrakt verletzen.
Unveränderlich: Wenn sich Formen nach ihrer Konstruktion nicht ändern können, muss ein quadratisches Objekt auch immer den Rechteckvertrag erfüllen. Ein Quadrat kann eine Beziehung zu einem Rechteck haben.
In beiden Fällen ist es möglich, ein Quadrat mit einer oder mehreren Änderungen zu bitten, eine neue Form basierend auf seinem Zustand zu erzeugen. Zum Beispiel könnte man sagen: "Erstellen Sie ein neues Rechteck basierend auf diesem Quadrat, außer dass die gegenüberliegenden Seiten A und C doppelt so lang sind." Da ein neues Objekt gebaut wird, bleibt der ursprüngliche Platz seinen Verträgen treu.
quelle
This is one of those cases where the real world is not able to be modeled in a computer 100%
. Warum so? Wir können immer noch ein Funktionsmodell eines Quadrats und eines Rechtecks haben. Die einzige Konsequenz ist, dass wir nach einem einfacheren Konstrukt suchen müssen, um diese beiden Objekte zu abstrahieren.Weil das Teil dessen ist, was es heißt, ein Subtyp zu sein (siehe auch: Liskov-Substitutionsprinzip). Sie können, müssen dazu in der Lage sein:
Sie tun dies die ganze Zeit (manchmal sogar impliziter), wenn Sie OOP verwenden.
Weil Sie diese nicht sinnvoll überschreiben können
Square
. Weil ein Quadrat nicht "wie ein Rechteck in der Größe verändert werden kann". Wenn sich die Höhe eines Rechtecks ändert, bleibt die Breite gleich. Wenn sich die Höhe eines Quadrats ändert, muss sich die Breite entsprechend ändern. Das Problem ist nicht nur in der Größe, sondern auch in beiden Dimensionen unabhängig voneinander zu ändern.quelle
Rect r = s;
Leitung, Sie können einfachdoSomethingWith(s)
und die Laufzeit verwendet alle Aufrufes
, um virtuelleSquare
Methoden aufzulösen .setWidth
undsetHeight
sowohl die Breite als auch die Höhe ändern.Was Sie beschreiben, verstößt gegen das so genannte Liskov-Substitutionsprinzip . Die Grundidee des LSP ist, dass Sie immer dann, wenn Sie eine Instanz einer bestimmten Klasse verwenden, in der Lage sein sollten, eine Instanz einer Unterklasse dieser Klasse auszutauschen, ohne Fehler einzuführen.
Das Rechteck-Quadrat-Problem ist kein guter Weg, um Liskov vorzustellen. Es wird versucht, ein breites Prinzip anhand eines tatsächlich recht subtilen Beispiels zu erklären, das einer der allgemeinsten intuitiven Definitionen in der gesamten Mathematik zuwiderläuft. Einige nennen es aus diesem Grund das Ellipse-Circle-Problem, aber diesbezüglich ist es nur geringfügig besser. Ein besserer Ansatz ist es, einen kleinen Schritt zurück zu machen, indem ich das sogenannte Parallelogramm-Rechteck-Problem benutze. Dies erleichtert das Verständnis.
Ein Parallelogramm ist ein Viereck mit zwei Paaren paralleler Seiten. Es hat auch zwei Paare von kongruenten Winkeln. Es ist nicht schwer, sich ein Parallelogramm-Objekt in dieser Richtung vorzustellen:
Ein Rechteck wird häufig als Parallelogramm mit rechten Winkeln betrachtet. Auf den ersten Blick scheint dies Rectangle zu einem guten Kandidaten für die Vererbung von Parallelogram zu machen , sodass Sie den ganzen leckeren Code wiederverwenden können. Jedoch:
Warum führen diese beiden Funktionen Fehler in Rectangle ein? Das Problem ist, dass Sie die Winkel in einem Rechteck nicht ändern können : Sie sind so definiert, dass sie immer 90 Grad betragen. Daher funktioniert diese Schnittstelle nicht für Rechtecke, die von Parallelogram erben. Wenn ich ein Rechteck in einen Code tausche, der ein Parallelogramm erwartet, und dieser Code versucht, den Winkel zu ändern, wird es mit ziemlicher Sicherheit Fehler geben. Wir haben etwas genommen, das in der Unterklasse beschreibbar und schreibgeschützt war, und das ist eine Liskov-Verletzung.
Wie wird dies nun auf Quadrate und Rechtecke angewendet?
Wenn wir sagen, dass Sie einen Wert festlegen können, meinen wir im Allgemeinen etwas Stärkeres, als nur einen Wert darauf schreiben zu können. Wir implizieren ein gewisses Maß an Exklusivität: Wenn Sie einen Wert festlegen und einige außergewöhnliche Umstände ausschließen, bleibt dieser Wert erhalten, bis Sie ihn erneut festlegen. Es gibt viele Verwendungszwecke für Werte, in die geschrieben werden kann, die jedoch nicht gesetzt bleiben. Es gibt jedoch auch viele Fälle, die davon abhängen, dass ein Wert dort bleibt, wo er ist, wenn Sie ihn gesetzt haben. Und hier stoßen wir auf ein anderes Problem.
Unsere Square-Klasse hat Fehler von Rectangle geerbt, aber es gibt einige neue. Das Problem bei setSideA und setSideB ist, dass keines dieser beiden Elemente mehr wirklich einstellbar ist: Sie können immer noch einen Wert in einen der beiden Werte schreiben, aber dieser ändert sich unter Ihnen, wenn der andere geschrieben wird. Wenn ich dies gegen ein Parallelogramm im Code eintausche, das davon abhängt, ob Seiten unabhängig voneinander gesetzt werden können, wird es ausflippen.
Das ist das Problem, und deshalb gibt es ein Problem bei der Verwendung von Rechteck als Einführung in Liskov. Rechteck hängt von dem Unterschied ab , ob man auf etwas schreiben und es setzen kann, und das ist ein viel subtilerer Unterschied, als wenn man etwas setzen kann und nicht, dass es schreibgeschützt ist. Rectangle-Square hat immer noch Wert als Beispiel, da es eine ziemlich häufige GOTCHA dokumentiert, die beachtet werden muss, aber nicht als einführendes Beispiel verwendet werden sollte. Lassen Sie den Lernenden zuerst etwas über die Grundlagen lernen und werfen Sie dann etwas Härteres auf ihn.
quelle
Beim Subtyping geht es um Verhalten.
Damit der Typ
B
ein Untertyp des Typs istA
, muss er jede OperationA
unterstützen , die der Typ mit derselben Semantik unterstützt (Fancy Talk für "Verhalten"). Die Begründung, dass jedes B ein A ist, funktioniert nicht - Verhaltenskompatibilität hat das letzte Wort. Meistens überschneidet sich "B ist eine Art A" mit "B verhält sich wie A", aber nicht immer .Ein Beispiel:
Betrachten Sie die Menge der reellen Zahlen. In jeder Sprache können wir erwarten , dass sie die Operationen unterstützen
+
,-
,*
, und/
. Betrachten Sie nun die Menge positiver Ganzzahlen ({1, 2, 3, ...}). Natürlich ist jede positive ganze Zahl auch eine reelle Zahl. Aber ist der Typ der positiven ganzen Zahlen ein Subtyp des Typs der reellen Zahlen? Schauen wir uns die vier Operationen an und sehen wir, ob sich positive Ganzzahlen genauso verhalten wie reelle Zahlen:+
: Wir können ohne Probleme positive ganze Zahlen hinzufügen.-
: Nicht alle Subtraktionen positiver Ganzzahlen führen zu positiven Ganzzahlen. Eg3 - 5
.*
: Wir können ohne Probleme positive ganze Zahlen multiplizieren./
: Wir können positive ganze Zahlen nicht immer teilen und eine positive ganze Zahl erhalten. Eg5 / 3
.Obwohl positive Ganzzahlen eine Teilmenge reeller Zahlen sind, sind sie kein Untertyp. Ein ähnliches Argument kann für ganze Zahlen endlicher Größe angeführt werden. Natürlich ist jede 32-Bit-Ganzzahl auch eine 64-Bit-Ganzzahl, aber
32_BIT_MAX + 1
Sie erhalten für jeden Typ unterschiedliche Ergebnisse. Wenn ich Ihnen also ein Programm gegeben habe und Sie den Typ jeder 32-Bit-Ganzzahlvariablen in 64-Bit-Ganzzahlen geändert haben, besteht eine gute Chance, dass sich das Programm anders verhält (was fast immer falsch bedeutet ).Natürlich können Sie auch
+
32-Bit-Ints definieren, sodass das Ergebnis eine 64-Bit-Ganzzahl ist. Jetzt müssen Sie jedoch jedes Mal 64 Bit Speicherplatz reservieren, wenn Sie zwei 32-Bit-Zahlen hinzufügen. Dies kann je nach Speicherbedarf für Sie akzeptabel sein oder auch nicht.Warum ist das wichtig?
Es ist wichtig, dass Programme korrekt sind. Es ist wohl die wichtigste Eigenschaft für ein Programm. Wenn ein Programm für einen bestimmten Typ korrekt ist
A
, ist die einzige Möglichkeit, sicherzustellen, dass das Programm für einen bestimmten Subtyp weiterhin korrektB
ist, dasB
VerhaltenA
in jeder Hinsicht.Sie haben also den Typ von
Rectangles
, dessen Spezifikation besagt, dass seine Seiten unabhängig voneinander geändert werden können. Sie haben einige Programme geschrieben,Rectangles
die die Spezifikation verwenden und davon ausgehen, dass die Implementierung der Spezifikation folgt. Dann haben Sie einen Untertyp namens eingeführt,Square
dessen Seiten nicht unabhängig voneinander angepasst werden können. Infolgedessen sind die meisten Programme, die die Größe von Rechtecken ändern, jetzt falsch.quelle
Fragen Sie sich zuerst, warum ein Quadrat ein Rechteck ist.
Natürlich haben die meisten Leute das in der Grundschule gelernt, und es scheint offensichtlich. Ein Rechteck ist eine vierseitige Form mit 90-Grad-Winkeln, und ein Quadrat erfüllt alle diese Eigenschaften. Ist ein Quadrat also kein Rechteck?
Die Sache ist jedoch, dass alles davon abhängt, nach welchen Anfangskriterien Sie Objekte gruppieren und in welchem Kontext Sie diese Objekte betrachten. In der Geometrie werden Formen anhand der Eigenschaften ihrer Punkte, Linien und Engel klassifiziert.
Bevor Sie also sagen, dass "ein Quadrat eine Art Rechteck ist", müssen Sie sich zunächst fragen, ob dies auf Kriterien beruht, die mir wichtig sind .
In den allermeisten Fällen ist es nicht das, was Sie interessiert. Die Mehrheit der Systeme, die Formen modellieren, wie z. B. GUIs, Grafiken und Videospiele, befassen sich nicht primär mit der geometrischen Gruppierung eines Objekts, sondern mit dem Verhalten. Haben Sie jemals an einem System gearbeitet, bei dem es darauf ankam, dass ein Quadrat eine Art Rechteck im geometrischen Sinne ist? Was würde dir das überhaupt bringen, wenn du wüsstest, dass es 4 Seiten und 90 Grad Winkel hat?
Sie modellieren kein statisches System, Sie modellieren ein dynamisches System, in dem Dinge passieren werden (Formen werden erzeugt, zerstört, verändert, gezeichnet usw.). In diesem Zusammenhang geht es Ihnen um das gemeinsame Verhalten von Objekten, da es Ihnen in erster Linie darum geht, was Sie mit einer Form tun können und welche Regeln eingehalten werden müssen, um ein kohärentes System zu erhalten.
In diesem Zusammenhang ist ein Quadrat definitiv kein Rechteck , da die Regeln, nach denen das Quadrat geändert werden kann, nicht mit denen des Rechtecks übereinstimmen. Sie sind also nicht dasselbe.
In diesem Fall modellieren Sie sie nicht als solche. Warum würdest du? Es bringt Ihnen nichts anderes als eine unnötige Einschränkung.
Wenn Sie das tun, obwohl Sie praktisch im Code angeben, dass sie nicht dasselbe sind. Ihr Code würde sagen, ein Quadrat verhält sich so und ein Rechteck verhält sich so, aber sie sind immer noch gleich.
Sie sind in dem Kontext, den Sie interessieren, eindeutig nicht gleich, weil Sie gerade zwei verschiedene Verhaltensweisen definiert haben. Warum also so tun, als wären sie gleich, wenn sie sich nur in einem Kontext ähneln, den Sie nicht interessieren?
Dies hebt ein erhebliches Problem hervor, wenn Entwickler zu einer Domäne gelangen, die sie modellieren möchten. Es ist sehr wichtig zu klären, an welchem Kontext Sie interessiert sind, bevor Sie über die Objekte in der Domäne nachdenken. Für welchen Aspekt interessieren Sie sich? Vor Tausenden von Jahren haben sich die Griechen um die gemeinsamen Eigenschaften der Linien und Engel von Formen gekümmert und sie danach gruppiert. Das bedeutet nicht, dass Sie gezwungen sind, diese Gruppierung fortzusetzen, wenn es Ihnen nicht wichtig ist (was Sie in 99% der Fälle bei der Modellierung in Software nicht interessiert).
Viele der Antworten auf diese Frage konzentrieren sich darauf, dass es beim Untertippen um das Gruppieren von Verhalten geht , da sie die Regeln sind .
Aber es ist so wichtig zu verstehen, dass Sie dies nicht nur tun, um die Regeln zu befolgen. Sie tun dies, weil es Ihnen in den allermeisten Fällen auch wirklich wichtig ist. Es ist dir egal, ob ein Quadrat und ein Rechteck die gleichen inneren Engel haben. Sie kümmern sich darum, was sie können, während sie noch Quadrate und Rechtecke sind. Sie interessieren sich für das Verhalten der Objekte, da Sie ein System modellieren, das sich darauf konzentriert, das System basierend auf den Verhaltensregeln der Objekte zu ändern.
quelle
Rectangle
nur zur Darstellung von Werten verwendet werden , kann es sein, dass eine Klasse ihren VertragSquare
erbtRectangle
und vollständig einhält. Leider unterscheiden viele Sprachen nicht zwischen Variablen, die Werte einschließen, und solchen, die Entitäten identifizieren.Square
Typ, der von einem unveränderlichenRectnagle
Typ erbt, könnte nützlich sein, wenn es einige Arten von Operationen gibt, die nur auf Quadraten ausgeführt werden können. Als ein realistisches Beispiel des Konzepts betrachtenReadableMatrix
wir einen Typ [Basistyp ein rechteckiges Array, das auf verschiedene Arten, einschließlich sparsam gespeichert werden kann] und eineComputeDeterminant
Methode. Es könnte sinnvoll seinComputeDeterminant
, nur mit einemReadableSquareMatrix
Typ zu arbeiten, der von einem abgeleitet istReadableMatrix
, was ich als Beispiel für eineSquare
Ableitung von einem betrachten würdeRectangle
.Das Problem liegt in der Überlegung, dass Dinge, die in der Realität in irgendeiner Weise zusammenhängen, nach der Modellierung genauso zusammenhängen müssen.
Das Wichtigste bei der Modellierung ist, die gemeinsamen Attribute und das gemeinsame Verhalten zu identifizieren, sie in der Basisklasse zu definieren und den untergeordneten Klassen zusätzliche Attribute hinzuzufügen.
Das Problem mit Ihrem Beispiel ist, dass es völlig abstrakt ist. Solange niemand weiß, wofür Sie diese Klassen verwenden möchten, ist es schwer zu erraten, welches Design Sie erstellen sollten. Aber wenn Sie wirklich nur Höhe, Breite und Größe haben möchten, wäre es logischer:
width
Parameter undresize(double factor)
ändern Sie die Breite um den angegebenen Faktorheight
und ihreresize
Funktion überschreibt , diesuper.resize
die Höhe aufruft und dann um den angegebenen Faktor ändertAus der Sicht der Programmierung gibt es in Square nichts, was Rectangle nicht hat. Es macht keinen Sinn, ein Quadrat als Unterklasse von Rechteck zu definieren.
quelle
Weil durch LSP das Erstellen einer Vererbungsbeziehung zwischen den beiden und das Überschreiben
setWidth
undsetHeight
Sicherstellen, dass das Quadrat beide hat, zu einem verwirrenden und nicht intuitiven Verhalten führt. Nehmen wir an, wir haben einen Code:Aber wenn die Methode
createRectangle
zurückgegeben wirdSquare
, ist es dank desSquare
Erbens von möglichRectange
. Dann werden die Erwartungen gebrochen. In diesem Code wird davon ausgegangen, dass die Einstellung von Breite oder Höhe nur zu einer Änderung der Breite bzw. Höhe führt. Der Punkt von OOP ist, dass Sie bei der Arbeit mit der Oberklasse keine Kenntnis von einer Unterklasse haben, die sich darunter befindet. Und wenn die Unterklasse das Verhalten so ändert, dass es den Erwartungen an die Superklasse widerspricht, besteht eine hohe Wahrscheinlichkeit, dass Fehler auftreten. Und diese Art von Fehlern ist sowohl schwer zu debuggen als auch zu beheben.Eine der Hauptideen von OOP ist, dass es Verhalten ist, nicht Daten, die vererbt werden (was auch eine der Hauptfehlvorstellungen von IMO ist). Und wenn Sie sich Quadrat und Rechteck ansehen, haben sie selbst kein Verhalten, das wir in Bezug auf die Vererbung in Beziehung setzen könnten.
quelle
Was LSP sagt, ist, dass alles, was von erbt, a sein
Rectangle
mussRectangle
. Das heißt, es sollte tun, was auch immer einRectangle
tut.Wahrscheinlich wird in der Dokumentation für
Rectangle
geschrieben, dass das Verhalten einesRectangle
Namensr
wie folgt ist:Wenn Ihr Square nicht dasselbe Verhalten aufweist, verhält es sich nicht wie ein
Rectangle
. Also sagt LSP, dass es nicht von erben darfRectangle
. Die Sprache kann diese Regel nicht durchsetzen, da sie nicht verhindern kann, dass Sie bei einer Methodenüberschreibung etwas falsch machen. Das bedeutet jedoch nicht, dass "es in Ordnung ist, weil ich die Methoden in der Sprache überschreiben kann", sondern ist ein überzeugendes Argument dafür!Nun wäre es möglich , die Dokumentation für
Rectangle
so zu schreiben , dass nicht impliziert wird, dass der obige Code 10 ausgibt. In diesem FallSquare
könnte es sich möglicherweise um eine handelnRectangle
. Möglicherweise sehen Sie eine Dokumentation, in der etwa steht: "Das macht X. Außerdem macht die Implementierung in dieser Klasse Y". Wenn ja, dann haben Sie ein gutes Argument, um eine Schnittstelle aus der Klasse zu extrahieren und zu unterscheiden, was die Schnittstelle garantiert und was die Klasse zusätzlich garantiert. Wenn die Leute sagen, "ein veränderliches Quadrat ist kein veränderliches Rechteck, während ein unveränderliches Quadrat ein unveränderliches Rechteck ist", nehmen sie im Grunde genommen an, dass das Obige tatsächlich Teil der vernünftigen Definition eines veränderlichen Rechtecks ist.quelle
Subtypen und OO-Programmierung basieren häufig auf dem Liskov-Substitutionsprinzip, dass jeder Wert des Typs A verwendet werden kann, wenn ein B erforderlich ist, wenn A <= B. Dies ist so ziemlich ein Axiom in der OO-Architektur, d. H. Es wird davon ausgegangen, dass alle Unterklassen über diese Eigenschaft verfügen (andernfalls sind die Untertypen fehlerhaft und müssen behoben werden).
Es stellt sich jedoch heraus, dass dieses Prinzip für die meisten Codes entweder unrealistisch / nicht repräsentativ ist oder in der Tat nicht zu befriedigen ist (in nicht trivialen Fällen)! Dieses Problem, bekannt als das Quadrat-Rechteck-Problem oder das Kreis-Ellipsen-Problem ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ), ist ein berühmtes Beispiel dafür, wie schwierig es ist, es zu lösen .
Beachten Sie, dass wir immer mehr beobachtungsäquivalente Quadrate und Rechtecke implementieren könnten , aber nur, indem wir immer mehr Funktionen wegwerfen, bis die Unterscheidung unbrauchbar wird.
Ein Beispiel finden Sie unter http://okmij.org/ftp/Computation/Subtyping/
quelle