Warum ist es problematisch, Square von Rectangle zu erben, wenn wir die Methoden SetWidth und SetHeight überschreiben?


105

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.


15
Diese Frage scheint schrecklich abstrakt. Es gibt unzählige Möglichkeiten, Klassen und Vererbung zu verwenden. Ob eine Klasse von einer Klasse geerbt wird oder nicht, hängt in der Regel davon ab, wie Sie diese Klassen verwenden möchten. Ohne einen praktischen Fall kann ich nicht sehen, wie diese Frage eine relevante Antwort erhalten kann.
aaaaaaaaaaa

2
Etwas gesundem Menschenverstand es sei daran erinnert, dass Platz ist ein Rechteck, so dass , wenn Objekt Ihrer Quadrat - Klasse kann nicht verwendet werden , in dem Rechteck es erforderlich ist , ist wahrscheinlich sowieso einige Anwendungsdesignfehler.
Cthulhu

7
Ich denke die bessere Frage ist 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.
Gusdor

2
@eBusiness Seine Abstraktheit macht es zu einer guten Lernfrage. Es ist wichtig zu erkennen, welche Verwendungen von Untertypen unabhängig von bestimmten Anwendungsfällen schlecht sind.
Doval

5
@ Cthulhu Nicht wirklich. Bei der Untertypisierung dreht sich alles um Verhalten, und ein veränderbares Quadrat verhält sich nicht wie ein veränderbares Rechteck. Deshalb ist die Metapher "is a ..." schlecht.
Doval

Antworten:


189

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.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

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:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

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 IncreaseRectangleSizeByTenPercentVerfahren 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:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

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 SideLengthEigentum und Rechtecken haben Lengthund WidthEigentum 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 Sie sealIhre 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.IdFeld vom alten Kunden zum neuen Kunden, sondern erstellen einen neuen Customer.

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.


7
Liskov ist eine Sache, und Lagerung ist eine andere Frage. In den meisten Implementierungen benötigt eine Square-Instanz, die von Rectangle erbt, Speicherplatz zum Speichern von zwei Dimensionen, obwohl nur eine benötigt wird.
el.pescado

29
Brillante Verwendung einer Geschichte, um den Punkt zu veranschaulichen
Rory Hunter

29
Schöne Geschichte, aber ich stimme nicht zu. Der Anwendungsfall war: Ändern Sie den Bereich eines Rechtecks. Das Update sollte eine überschreibbare Methode 'ChangeArea' zum Rechteck hinzufügen, die sich auf Square spezialisiert. Dies würde die Vererbungskette nicht unterbrechen, nicht explizit angeben, was der Benutzer tun wollte, und würde nicht den Fehler verursachen, der durch das von Ihnen erwähnte Update (das in einem geeigneten Staging-Bereich abgefangen worden wäre) verursacht wurde.
Roy T.

33
@RoyT .: Warum sollte ein Rechteck wissen, wie es seine Fläche festlegt ? Dies ist eine Eigenschaft, die sich ausschließlich aus der Länge und Breite ergibt. Und mehr auf den Punkt, welche Dimension sollte es ändern - die Länge, die Breite oder beides?
CHAO

32
@Roy T. Es ist alles sehr schön zu sagen, dass Sie das Problem anders gelöst hätten, aber die Tatsache ist, dass dies ein - wenn auch vereinfachtes - Beispiel für reale Situationen ist, mit denen Entwickler täglich konfrontiert sind, wenn sie ältere Produkte warten. Und selbst wenn Sie diese Methode implementiert haben, wird dies nicht verhindern, dass Erben den LSP verletzen und ähnliche Fehler einführen. Aus diesem Grund ist so gut wie jede Klasse im .NET Framework versiegelt.
Stephen

30

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 ändern setWidth, wird die Spezifikation von Rechtecken nicht implementiert setWidth. 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:

  • Geben Sie nur Werte zurück, die die Superklasse zurückgeben würde. Dies bedeutet, dass der Rückgabetyp ein Subtyp des Rückgabetyps der Superklasse-Methode sein muss. Die Rückgabe ist eine Co-Variante.
  • Akzeptieren Sie alle Werte, die der Supertyp akzeptieren würde - das heißt, die Argumenttypen müssen Supertypen der Argumenttypen der Superklassenmethode sein. Argumente sind gegenvariant.

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 und compareTo(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.


„Wenn alle Objekte unveränderlich sind, gibt es kein Problem“ - das im Rahmen dieser Frage offenbar irrelevant Aussage ist, die Setter für Breite und Höhe explizit erwähnt
gnat

11
Ich fand das interessant, auch wenn es "anscheinend irrelevant" ist
Jesvin Jose

14
@gnat Ich würde behaupten, es ist relevant, weil der wahre Wert der Frage darin besteht, zu erkennen, ob zwischen zwei Typen eine gültige Untertypisierungsbeziehung besteht. Das hängt davon ab, welche Operationen der Supertyp deklariert. Es lohnt sich also darauf hinzuweisen, dass das Problem verschwindet, wenn die Mutator-Methoden verschwinden.
Doval

1
@gnat auch, Setter sind Mutatoren , also sagt lrn im Wesentlichen: "Tu das nicht und es ist kein Problem." Ich stimme der Unveränderlichkeit für einfache Typen zu, aber Sie machen einen guten Punkt: Bei komplexen Objekten ist das Problem nicht so einfach.
Patrick M

1
Betrachten Sie es so, wie wird das Verhalten von der Klasse "Rechteck" garantiert? Dass Sie Breite und Höhe UNABHÄNGIG voneinander ändern können. (dh setWidth und setHeight) -Methode. Wenn Square von Rectangle abgeleitet ist, muss Square dieses Verhalten garantieren. Da square dieses Verhalten nicht garantieren kann, ist es eine schlechte Vererbung. Wenn jedoch die setWidth / setHeight-Methoden aus der Rectangle-Klasse entfernt werden, gibt es kein solches Verhalten, und Sie können die Square-Klasse von Rectangle ableiten.
Nitin Bhide

17

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:

  • Ein Quadrat ist eine besondere Art von Raute - es ist eine Raute mit quadratischen Winkeln.
  • Eine Raute ist eine besondere Art von Parallelogramm - es ist ein Parallelogramm mit gleichen Seiten.
  • Ein Rechteck ist eine spezielle Art von Parallelogramm - es ist ein Parallelogramm mit quadratischen Winkeln
  • Ein Rechteck, ein Quadrat und ein Parallelogramm sind alle eine besondere Art von Trapez - sie sind Trapezoide mit zwei Sätzen paralleler Seiten
  • Alle oben genannten sind spezielle Arten von Vierecken
  • Alle oben genannten sind spezielle Arten von planaren Formen
  • Und so weiter; Ich könnte noch einige Zeit hier weitermachen.

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.


3
Wenn Shapes-Objekte unveränderlich sind, kann es sich um einen IShapeTyp handeln, der einen Begrenzungsrahmen enthält, der gezeichnet, skaliert und serialisiert werden kann, und einen IPolygonSubtyp mit einer Methode zum Melden der Anzahl von Scheitelpunkten und einer Methode zum Zurückgeben von IEnumerable<Point>. Man könnte dann IQuadrilateralSubtyp , die aus leitet IPolygon, IRhombusund IRectangleleiten daraus, und ISquareentstammen IRhombusund IRectangle. 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.
Supercat

Ich stimme Eric hier nicht zu (aber nicht genug für eine -1!). Alle diese Beziehungen sind (möglicherweise) relevant, wie @supercat erwähnt; Es ist nur ein YAGNI-Problem: Sie implementieren es erst, wenn Sie es brauchen.
Mark Hurd

Sehr gute Antwort! Sollte viel höher sein.
andrew.fox

1
@ MarkHurd - Es handelt sich nicht um ein YAGNI-Problem: Die vorgeschlagene vererbungsbasierte Hierarchie ist wie die beschriebene Taxonomie gestaltet, kann jedoch nicht geschrieben werden, um die Beziehungen zu garantieren, die sie definieren. Wie kann IRhombussichergestellt werden, dass alle Pointvon dem Enumerable<Point>definierten durch zurückgegebenen IPolygonKanten gleicher Länge entsprechen? Da die Implementierung der IRhombusSchnittstelle allein nicht garantiert, dass ein konkretes Objekt eine Raute ist, kann Vererbung nicht die Antwort sein.
A. Rager

14

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.


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

6
Es gibt mehr Gemeinsamkeiten zwischen Rechtecken und Quadraten. Das Problem ist, dass die Identität eines Rechtecks ​​und die Identität eines Quadrats die Seitenlängen (und der Winkel an jedem Schnittpunkt) sind. Die beste Lösung ist, Quadrate von Rechtecken zu erben, aber beide unveränderlich zu machen.
Stephen

3
@ Stephen Einverstanden. Tatsächlich ist es sinnvoll, sie unveränderlich zu machen, unabhängig von Subtypisierungsproblemen. Es gibt keinen Grund, sie veränderbar zu machen - es ist nicht schwieriger, ein neues Quadrat oder ein neues Rechteck zu konstruieren, als eines zu mutieren. Warum also diese Dose mit Würmern öffnen? Sie müssen sich jetzt nicht mehr um Aliasing / Nebenwirkungen kümmern und können diese bei Bedarf als Schlüssel für Karten / Dikte verwenden. Einige sagen "Leistung", zu der ich "vorzeitige Optimierung" sage, bis sie tatsächlich gemessen und bewiesen haben, dass der Hot Spot im Formcode enthalten ist.
Doval

Sorry Leute, es war spät und ich war super müde, als ich die Antwort schrieb. Ich habe es umgeschrieben, um zu sagen, was ich wirklich gemeint habe. Der springende Punkt ist die Veränderlichkeit.

13

Und warum sollte Square überall dort einsetzbar sein, wo Sie ein Rechteck erwarten?

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:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

Sie tun dies die ganze Zeit (manchmal sogar impliziter), wenn Sie OOP verwenden.

und wenn wir die Methoden SetWidth und SetHeight für Square außer Kraft setzen, warum sollte es dann ein Problem geben?

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.


In vielen Sprachen brauchen Sie nicht einmal die Rect r = s;Leitung, Sie können einfach doSomethingWith(s)und die Laufzeit verwendet alle Aufrufe s, um virtuelle SquareMethoden aufzulösen .
Patrick M

1
@PatrickM Sie brauchen es nicht in einer vernünftigen Sprache, die Untertypen hat. Ich habe diese Zeile zur Erläuterung eingefügt, um es deutlich zu machen.

Also außer Kraft setzen setWidthund setHeightsowohl die Breite als auch die Höhe ändern.
ApproachingDarknessFish

@ValekHalfHeart Das ist genau die Option, die ich in Betracht ziehe.

7
@ValekHalfHeart: Das ist genau die Verletzung des Liskov-Substitutionsprinzips, die Sie heimsuchen wird und Sie zwei Jahre später mehrere schlaflose Nächte damit verbringen wird, einen seltsamen Fehler zu finden, wenn Sie vergessen haben, wie der Code funktionieren soll.
Jan Hudec

9

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:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

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:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

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.

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

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.


8

Beim Subtyping geht es um Verhalten.

Damit der Typ Bein Untertyp des Typs ist A, muss er jede Operation Aunterstü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. Eg 3 - 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. Eg 5 / 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 + 1Sie 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 korrekt Bist, das BVerhalten Ain 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, Rectanglesdie die Spezifikation verwenden und davon ausgehen, dass die Implementierung der Spezifikation folgt. Dann haben Sie einen Untertyp namens eingeführt, Squaredessen Seiten nicht unabhängig voneinander angepasst werden können. Infolgedessen sind die meisten Programme, die die Größe von Rechtecken ändern, jetzt falsch.


6

Wenn ein Quadrat eine Art Rechteck ist, warum kann ein Quadrat dann nicht von einem Rechteck erben? Oder warum ist es ein schlechtes Design?

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.

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


Wenn Variablen vom Typ Rectanglenur zur Darstellung von Werten verwendet werden , kann es sein, dass eine Klasse ihren Vertrag Squareerbt Rectangleund vollständig einhält. Leider unterscheiden viele Sprachen nicht zwischen Variablen, die Werte einschließen, und solchen, die Entitäten identifizieren.
Supercat

Möglicherweise, aber warum dann überhaupt? Der Punkt des Rechteck / Quadrat-Problems besteht nicht darin, herauszufinden, wie die Beziehung "Ein Quadrat ist ein Rechteck" funktioniert, sondern zu erkennen, dass die Beziehung in dem Kontext, in dem Sie die Objekte verwenden, tatsächlich nicht vorhanden ist (verhaltensbezogen) und als Warnung davor, Ihrer Domain nicht übermäßig irrelevante Beziehungen aufzuzwingen.
Cormac Mulhall

Oder anders ausgedrückt: Versuchen Sie nicht, den Löffel zu biegen. Das ist unmöglich. Versuche stattdessen nur die Wahrheit zu erkennen, dass es keinen Löffel gibt. :-)
Cormac Mulhall

1
Ein unveränderlicher SquareTyp, der von einem unveränderlichen RectnagleTyp 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 betrachten ReadableMatrixwir einen Typ [Basistyp ein rechteckiges Array, das auf verschiedene Arten, einschließlich sparsam gespeichert werden kann] und eine ComputeDeterminantMethode. Es könnte sinnvoll sein ComputeDeterminant, nur mit einem ReadableSquareMatrixTyp zu arbeiten, der von einem abgeleitet ist ReadableMatrix, was ich als Beispiel für eine SquareAbleitung von einem betrachten würde Rectangle.
Supercat

5

Wenn ein Quadrat eine Art Rechteck ist, warum kann ein Quadrat dann nicht von einem Rechteck erben?

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:

  • Definieren Sie Square als Basisklasse mit widthParameter und resize(double factor)ändern Sie die Breite um den angegebenen Faktor
  • Definieren Sie die Rectangle-Klasse und die Unterklasse von Square, da sie ein weiteres Attribut hinzufügt heightund ihre resizeFunktion überschreibt , die super.resizedie Höhe aufruft und dann um den angegebenen Faktor ändert

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


+1 Nur weil ein Quadrat in der Mathematik eine besondere Art von Rechteck ist, heißt das nicht, dass es in OO dasselbe ist.
Lovis

1
Ein Quadrat ist ein Quadrat und ein Rechteck ist ein Rechteck. Die Beziehungen zwischen ihnen sollten auch beim Modellieren Bestand haben, sonst haben Sie ein ziemlich schlechtes Modell. Die eigentlichen Probleme sind: 1) Wenn Sie sie veränderbar machen, modellieren Sie keine Quadrate und Rechtecke mehr. 2) Unter der Annahme, dass nur weil eine "ist eine" Beziehung zwischen zwei Arten von Objekten besteht, können Sie eines wahllos durch das andere ersetzen.
Doval

4

Weil durch LSP das Erstellen einer Vererbungsbeziehung zwischen den beiden und das Überschreiben setWidthund setHeightSicherstellen, dass das Quadrat beide hat, zu einem verwirrenden und nicht intuitiven Verhalten führt. Nehmen wir an, wir haben einen Code:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

Aber wenn die Methode createRectanglezurückgegeben wird Square, ist es dank des SquareErbens von möglich Rectange. 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.


2

Was LSP sagt, ist, dass alles, was von erbt, a sein Rectanglemuss Rectangle. Das heißt, es sollte tun, was auch immer ein Rectangletut.

Wahrscheinlich wird in der Dokumentation für Rectanglegeschrieben, dass das Verhalten eines RectangleNamens rwie folgt ist:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Wenn Ihr Square nicht dasselbe Verhalten aufweist, verhält es sich nicht wie ein Rectangle . Also sagt LSP, dass es nicht von erben darf Rectangle. 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 Rectangleso zu schreiben , dass nicht impliziert wird, dass der obige Code 10 ausgibt. In diesem Fall Squarekönnte es sich möglicherweise um eine handeln Rectangle. 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.


dies scheint nur Punkte zu wiederholen in einer erklärten Antwort gepostet vor 5 Stunden
gnat

@gnat: Würden Sie es vorziehen, wenn ich diese andere Antwort bis auf ungefähr diese Kürze bearbeite? ;-) Ich glaube nicht, dass ich es kann, ohne Punkte zu entfernen, die andere Antworter vermutlich für notwendig halten, um die Frage zu beantworten, und ich denke, dass dies nicht der Fall ist.
Steve Jessop


1

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/

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.