Ich verfolgte diese hoch gestimmte Frage bezüglich eines möglichen Verstoßes gegen das Liskov-Substitutionsprinzip. Ich weiß, was das Liskov-Substitutionsprinzip ist, aber was mir immer noch nicht klar ist, kann schief gehen, wenn ich als Entwickler beim Schreiben von objektorientiertem Code nicht über das Prinzip nachdenke.
object-oriented
object-oriented-design
solid
design-principles
liskov-substitution
Aussenseiter
quelle
quelle
Antworten:
Ich denke, es ist sehr gut in dieser Frage ausgedrückt, was einer der Gründe ist, die so hoch gewählt wurden.
Stellen Sie sich vor, Sie wollen:
In dieser Methode wird gelegentlich der Aufruf von .Close () ausgelöst. Aufgrund der konkreten Implementierung eines abgeleiteten Typs müssen Sie nun das Verhalten dieser Methode ändern, das sich davon unterscheidet, wie diese Methode geschrieben würde, wenn Task keine Untertypen hätte, die vorhanden sein könnten an diese Methode übergeben.
Aufgrund von Verstößen gegen die Liskov-Substitution muss der Code, der Ihren Typ verwendet, über explizite Kenntnisse der internen Funktionsweise abgeleiteter Typen verfügen, um sie unterschiedlich zu behandeln. Dies koppelt den Code eng miteinander und erschwert im Allgemeinen die konsistente Verwendung der Implementierung.
quelle
Wenn Sie den in der Basisklasse definierten Vertrag nicht erfüllen, kann es zu einem unbeaufsichtigten Fehler kommen, wenn Sie fehlerhafte Ergebnisse erhalten.
LSP in Wikipedia Staaten
Sollte einer dieser Punkte nicht zutreffen, erhält der Anrufer möglicherweise ein Ergebnis, das er nicht erwartet.
quelle
Betrachten Sie einen klassischen Fall aus den Annalen der Interviewfragen: Sie haben Circle von Ellipse abgeleitet. Warum? Weil ein Kreis natürlich eine Ellipse ist!
Außer ... Ellipse hat zwei Funktionen:
Natürlich müssen diese für Kreis neu definiert werden, da ein Kreis einen einheitlichen Radius hat. Sie haben zwei Möglichkeiten:
Die meisten OO-Sprachen unterstützen nicht die zweite, und das aus einem guten Grund: Es wäre überraschend, wenn Ihr Kreis kein Kreis mehr ist. Die erste Option ist also die beste. Beachten Sie jedoch die folgende Funktion:
Stellen Sie sich vor, dass eine Funktion e.set_alpha_radius aufruft. Aber da es sich bei e wirklich um einen Kreis handelte, wurde überraschenderweise auch der Beta-Radius festgelegt.
Und hier liegt das Substitutionsprinzip: Eine Unterklasse muss für eine Oberklasse substituierbar sein. Ansonsten passiert überraschendes.
quelle
Mit den Worten des Laien:
Ihr Code wird eine Menge CASE / switch-Klauseln enthalten .
Für jede dieser CASE / switch-Klauseln müssen von Zeit zu Zeit neue Fälle hinzugefügt werden, was bedeutet, dass die Codebasis nicht so skalierbar und wartbar ist, wie sie sein sollte.
Mit LSP funktioniert Code eher wie Hardware:
Sie müssen Ihren iPod nicht modifizieren, da Sie ein neues Paar externer Lautsprecher gekauft haben. Da sowohl der alte als auch der neue externe Lautsprecher dieselbe Schnittstelle verwenden, können sie ausgetauscht werden, ohne dass der iPod die gewünschte Funktionalität verliert.
quelle
typeof(someObject)
zu entscheiden, was Sie "dürfen", dann sicher, aber das ist ein ganz anderes Anti-Pattern.um mit dem UndoManager von Java ein reales Beispiel zu geben
es erbt von
AbstractUndoableEdit
wessen Vertrag angibt, dass es 2 Zustände hat (rückgängig gemacht und wiederholt) und zwischen diesen mit einzelnen Aufrufen zuundo()
und wechseln kannredo()
Der UndoManager hat jedoch mehr Status und verhält sich wie ein Rückgängig-Puffer (jeder Aufruf
undo
macht einige, aber nicht alle Änderungen rückgängig , wodurch die Nachbedingung geschwächt wird).Dies führt zu der hypothetischen Situation, dass Sie einem CompoundEdit einen UndoManager hinzufügen, bevor Sie
end()
Undo aufrufen. Dieser CompoundEdit ruft dann Undo auf und ruftundo()
jede Bearbeitung auf, sobald Ihre Änderungen teilweise rückgängig gemacht wurdenIch habe mein eigenes
UndoManager
gewürfelt, um das zu vermeiden (ich sollte es aber wahrscheinlich in umbenennenUndoBuffer
)quelle
Beispiel: Sie arbeiten mit einem UI-Framework und erstellen Ihr eigenes benutzerdefiniertes UI-Steuerelement, indem Sie die
Control
Basisklasse in Unterklassen unterteilen. DieControl
Basisklasse definiert ein Verfahren ,getSubControls()
das soll eine Sammlung von verschachtelten Steuerelemente (falls vorhanden) zurückzukehren. Sie überschreiben jedoch die Methode, um tatsächlich eine Liste der Geburtsdaten der Präsidenten der Vereinigten Staaten zurückzugeben.Was kann damit schief gehen? Es ist offensichtlich, dass das Rendern des Steuerelements fehlschlägt, da Sie nicht wie erwartet eine Liste der Steuerelemente zurückgeben. Höchstwahrscheinlich stürzt die Benutzeroberfläche ab. Sie brechen den Vertrag, den Unterklassen von Control einhalten sollen.
quelle
Sie können es auch unter dem Gesichtspunkt der Modellierung betrachten. Wenn Sie sagen, dass eine Klasseninstanz
A
auch eine Klasseninstanz istB
, implizieren Sie, dass "das beobachtbare Verhalten einer KlasseninstanzA
auch als beobachtbares Verhalten einer Klasseninstanz klassifiziert werden kannB
" (Dies ist nur möglich, wenn die KlasseB
weniger spezifisch ist als KlasseA
.)Wenn Sie also gegen den LSP verstoßen, besteht ein Widerspruch in Ihrem Entwurf: Sie definieren einige Kategorien für Ihre Objekte und respektieren diese in Ihrer Implementierung nicht. Es muss etwas falsch sein.
Als würde man eine Schachtel mit einem Etikett erstellen: "Diese Schachtel enthält nur blaue Kugeln" und dann eine rote Kugel hineinwerfen. Was nützt ein solches Tag, wenn es die falschen Informationen enthält?
quelle
Ich habe kürzlich eine Codebasis geerbt, die einige wichtige Liskov-Übertreter enthält. In wichtigen Klassen. Das hat mir große Schmerzen bereitet. Lass mich erklären warum.
Ich habe
Class A
, was davon herrührtClass B
.Class A
undClass B
eine Reihe von Eigenschaften gemeinsam nutzen,Class A
die die eigene Implementierung überschreiben. Das Festlegen oder Abrufen einerClass A
Eigenschaft hat andere Auswirkungen als das Festlegen oder Abrufen derselben EigenschaftClass B
.Abgesehen von der Tatsache, dass dies eine äußerst schreckliche Methode für die Übersetzung in .NET ist, gibt es eine Reihe weiterer Probleme mit diesem Code.
In diesem Fall
Name
wird an mehreren Stellen ein Index und eine Flusssteuerungsvariable verwendet. Die oben genannten Klassen sind in der gesamten Codebasis sowohl in ihrer rohen als auch in ihrer abgeleiteten Form übersät. Wenn ich in diesem Fall gegen das Liskov-Substitutionsprinzip verstoße, muss ich den Kontext jedes einzelnen Aufrufs jeder der Funktionen kennen, die die Basisklasse verwenden.Der Code verwendet Objekte von beiden
Class A
undClass B
, so dass ich nicht einfachClass A
abstrakt machen kann , um Leute zur Verwendung zu zwingenClass B
.Es gibt einige sehr nützliche Dienstprogrammfunktionen, die ausgeführt werden,
Class A
und andere sehr nützliche Dienstprogrammfunktionen, die ausgeführt werdenClass B
. Im Idealfall würde Ich mag jede Nutzenfunktion verwenden können , die arbeiten auf könnenClass A
aufClass B
. Viele der Funktionen, die aClass B
benötigen, könnten a ohneClass A
die Verletzung des LSP problemlos in Anspruch nehmen.Das Schlimmste daran ist, dass sich dieser spezielle Fall nur schwer umgestalten lässt, da die gesamte Anwendung von diesen beiden Klassen abhängt, die ganze Zeit auf beiden Klassen ausgeführt wird und in hundertfacher Hinsicht einen Bruch darstellt, wenn ich dies ändere (was ich tun werde) sowieso).
Um dies zu beheben, muss ich eine
NameTranslated
Eigenschaft erstellen , die dieClass B
Version derName
Eigenschaft ist, und jeden Verweis auf die abgeleiteteName
Eigenschaft sehr, sehr sorgfältig ändern , um meine neueNameTranslated
Eigenschaft zu verwenden. Wenn jedoch auch nur eine dieser Referenzen falsch ist, kann die gesamte Anwendung in die Luft gehen.Da es in der Codebasis keine Komponententests gibt, ist dies beinahe das gefährlichste Szenario, dem ein Entwickler begegnen kann. Wenn ich die Verletzung nicht ändere, muss ich große Mengen geistiger Energie aufwenden, um zu verfolgen, welcher Objekttyp bei jeder Methode bearbeitet wird, und wenn ich die Verletzung behebe, kann das gesamte Produkt zu einem ungünstigen Zeitpunkt explodieren.
quelle
BaseName
undTranslatedName
sowohl auf den Klasse-A-StilName
als auch auf die Klasse-B-Bedeutung zugreifen würden ? Dann würde jeder Versuch,Name
auf eine Variable des Typs zuzugreifen,B
mit einem Compilerfehler zurückgewiesen, sodass Sie sicherstellen könnten, dass alle Verweise in eines der anderen Formulare konvertiert wurden.Wenn Sie das Problem der Verletzung von LSP spüren möchten, überlegen Sie, was passiert, wenn Sie nur DLL / JAR der Basisklasse (keinen Quellcode) haben und eine neue abgeleitete Klasse erstellen müssen. Sie können diese Aufgabe niemals ausführen.
quelle