Ich habe gehört, dass das Liskov-Substitutionsprinzip (LSP) ein Grundprinzip des objektorientierten Designs ist. Was ist es und was sind einige Beispiele für seine Verwendung?
908
Ich habe gehört, dass das Liskov-Substitutionsprinzip (LSP) ein Grundprinzip des objektorientierten Designs ist. Was ist es und was sind einige Beispiele für seine Verwendung?
Antworten:
Ein gutes Beispiel für LSP (von Onkel Bob in einem Podcast, den ich kürzlich gehört habe) war, dass manchmal etwas, das in natürlicher Sprache richtig klingt, im Code nicht ganz funktioniert.
In der Mathematik ist a
Square
aRectangle
. In der Tat ist es eine Spezialisierung eines Rechtecks. Das "ist ein" macht Lust, dies mit Vererbung zu modellieren. Wenn Sie jedoch in Code, aus dem SieSquare
stammenRectangle
, a ableiten möchten ,Square
sollte a überall dort verwendet werden können, wo Sie a erwartenRectangle
. Dies führt zu einem seltsamen Verhalten.Stellen Sie sich vor Sie hatten
SetWidth
undSetHeight
Methoden auf IhrerRectangle
Basisklasse; das scheint vollkommen logisch. Allerdings , wenn IhrRectangle
Hinweis auf eine spitzSquare
, dannSetWidth
undSetHeight
macht keinen Sinn , weil eine Einstellung würde den anderen ändern , es zu entsprechen. In diesem Fall bestehtSquare
der Liskov-Substitutionstest mit nichtRectangle
und die Abstraktion, von derSquare
geerbt wurde,Rectangle
ist schlecht.Sie sollten sich die anderen unbezahlbaren Motivationsposter von SOLID Principles ansehen .
quelle
Square.setWidth(int width)
es so implementiert würde :this.width = width; this.height = width;
? In diesem Fall ist garantiert, dass die Breite der Höhe entspricht.Das Liskov-Substitutionsprinzip (LSP, lsp) ist ein Konzept in der objektorientierten Programmierung, das Folgendes besagt:
Im Mittelpunkt von LSP stehen Schnittstellen und Verträge sowie die Entscheidung, wann eine Klasse erweitert werden soll, und die Verwendung einer anderen Strategie wie der Komposition, um Ihr Ziel zu erreichen.
Der effektivste Weg , den ich gesehen habe , diesen Punkt zu illustrieren war in Head First OOA & D . Sie stellen ein Szenario vor, in dem Sie Entwickler eines Projekts sind, um ein Framework für Strategiespiele zu erstellen.
Sie präsentieren eine Klasse, die ein Board darstellt, das so aussieht:
Alle Methoden verwenden X- und Y-Koordinaten als Parameter, um die Kachelposition in der zweidimensionalen Anordnung von zu lokalisieren
Tiles
. Auf diese Weise kann ein Spieleentwickler im Verlauf des Spiels Einheiten auf dem Brett verwalten.In dem Buch werden die Anforderungen dahingehend geändert, dass die Spielrahmenarbeit auch 3D-Spielbretter unterstützen muss, um flugfähige Spiele aufzunehmen. So wird eine
ThreeDBoard
Klasse eingeführt, die sich erweitertBoard
.Auf den ersten Blick scheint dies eine gute Entscheidung zu sein.
Board
bietet sowohl dieHeight
undWidth
-Eigenschaften als auchThreeDBoard
die Z-Achse.Wenn Sie sich alle anderen Mitglieder ansehen, von denen es geerbt wurde, bricht es zusammen
Board
. Die Methoden fürAddUnit
,GetTile
,GetUnits
und so weiter, nehmen alle X- und Y - Parameter in derBoard
Klasse , aber dasThreeDBoard
braucht einen Z - Parameter als auch.Sie müssen diese Methoden also erneut mit einem Z-Parameter implementieren. Der Z-Parameter hat keinen Kontext zur
Board
Klasse und die von derBoard
Klasse geerbten Methoden verlieren ihre Bedeutung. Eine Codeeinheit, die versucht, dieThreeDBoard
Klasse als Basisklasse zu verwenden,Board
hätte kein Glück.Vielleicht sollten wir einen anderen Ansatz finden. Anstatt zu erweitern
Board
,ThreeDBoard
sollte ausBoard
Objekten bestehen. EinBoard
Objekt pro Einheit der Z-Achse.Dies ermöglicht es uns, gute objektorientierte Prinzipien wie Kapselung und Wiederverwendung zu verwenden und verletzt LSP nicht.
quelle
Lassen Sie uns ein einfaches Beispiel in Java machen:
Schlechtes Beispiel
Die Ente kann fliegen, weil sie ein Vogel ist. Aber was ist damit:
Strauß ist ein Vogel, aber er kann nicht fliegen, Straußklasse ist ein Subtyp der Klasse Vogel, aber er kann die Fliegenmethode nicht verwenden, das heißt, wir brechen das LSP-Prinzip.
Gutes Beispiel
quelle
Bird bird
. Sie müssen das Objekt auf FlyingBirds übertragen, um Fly verwenden zu können, was nicht schön ist, oder?Bird bird
, bedeutet dies, dass er nicht verwenden kannfly()
. Das ist es. Das Bestehen von aDuck
ändert nichts an dieser Tatsache. Wenn der Client hatFlyingBirds bird
, sollte es auch dannDuck
immer so funktionieren , wenn es bestanden wird .LSP betrifft Invarianten.
Das klassische Beispiel ist die folgende Pseudocode-Deklaration (Implementierungen weggelassen):
Jetzt haben wir ein Problem, obwohl die Schnittstelle übereinstimmt. Der Grund ist, dass wir Invarianten verletzt haben, die sich aus der mathematischen Definition von Quadraten und Rechtecken ergeben. Die Art und Weise, wie Getter und Setter arbeiten,
Rectangle
sollte die folgende Invariante erfüllen:Diese Invariante muss jedoch durch eine korrekte Implementierung von verletzt werden
Square
, daher ist sie kein gültiger Ersatz fürRectangle
.quelle
Robert Martin hat ein ausgezeichnetes Papier über das Liskov-Substitutionsprinzip . Es werden subtile und nicht so subtile Möglichkeiten erörtert, wie das Prinzip verletzt werden kann.
Einige relevante Teile des Papiers (beachten Sie, dass das zweite Beispiel stark verdichtet ist):
quelle
Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.
Wenn eine Vorbedingung für eine Kinderklasse stärker ist als eine Vorbedingung für eine Elternklasse, können Sie ein Elternteil nicht durch ein Kind ersetzen, ohne die Vorbedingung zu verletzen. Daher LSP.LSP ist erforderlich, wenn ein Code denkt, dass er die Methoden eines Typs aufruft
T
, und möglicherweise unwissentlich die Methoden eines Typs aufruftS
, wobeiS extends T
(dhS
der Supertyp erbt, von ihm abgeleitet ist oder ein Subtyp davon istT
).Dies tritt beispielsweise auf, wenn eine Funktion mit einem Eingabeparameter vom Typ
T
mit einem Argumentwert vom Typ aufgerufen (dh aufgerufen) wirdS
. Oder wenn einem Bezeichner vom TypT
ein Wert vom Typ zugewiesen wirdS
.LSP erfordert, dass die Erwartungen (dh Invarianten) für Methoden vom Typ
T
(z. B.Rectangle
) nicht verletzt werden, wenn stattdessen die Methoden vom TypS
(z. B.Square
) aufgerufen werden.Sogar ein Typ mit unveränderlichen Feldern hat immer noch Invarianten, z. B. erwarten die unveränderlichen Rechteck-Setter, dass Dimensionen unabhängig voneinander geändert werden, aber die unveränderlichen Quadrat-Setter verletzen diese Erwartung.
LSP erfordert, dass jede Methode des Subtyps
S
kontravariante Eingabeparameter und eine kovariante Ausgabe hat.Kontravariante bedeutet, dass die Varianz der Richtung der Vererbung widerspricht, dh der Typ
Si
jedes Eingabeparameters jeder Methode des SubtypsS
muss derselbe sein oder ein Supertyp des TypsTi
des entsprechenden Eingabeparameters der entsprechenden Methode des SupertypsT
.Kovarianz bedeutet, dass die Varianz in der gleichen Richtung der Vererbung liegt, dh der Typ
So
der Ausgabe jeder Methode des SubtypsS
muss gleich sein oder ein Subtyp des TypsTo
der entsprechenden Ausgabe der entsprechenden Methode des SupertypsT
.Dies liegt daran, dass der Aufrufer, wenn er glaubt, einen Typ zu haben
T
, eine Methode aufruftT
, Argumente vom Typ liefertTi
und die Ausgabe dem Typ zuweistTo
. Wenn tatsächlich die entsprechende Methode von aufgerufen wirdS
, wird jedesTi
Eingabeargument einemSi
Eingabeparameter zugewiesen , und dieSo
Ausgabe wird dem Typ zugewiesenTo
. Wenn alsoSi
nicht kontravariant wäreTi
, könnte ein SubtypXi
- der kein Subtyp vonSi
wäre - zugewiesen werdenTi
.Zusätzlich für Sprachen (zB Scala oder Ceylon) , die Definition-site Varianz Annotationen auf Typ Polymorphismus Parameter haben (dh Generika), die Co- oder Wider- Richtung der Varianz Annotation für jeden Typ Parameter des Typs
T
müssen gegenüber oder gleiche Richtung jeweils zu jedem Eingabeparameter oder Ausgang (jeder Methode vonT
), der den Typ des Typparameters hat.Zusätzlich wird für jeden Eingabeparameter oder Ausgang, der einen Funktionstyp hat, die erforderliche Varianzrichtung umgekehrt. Diese Regel wird rekursiv angewendet.
Die Untertypisierung ist geeignet, wenn die Invarianten aufgezählt werden können.
Es wird viel darüber geforscht, wie Invarianten modelliert werden können, damit sie vom Compiler erzwungen werden.
Typestate (siehe Seite 3) deklariert und erzwingt Zustandsinvarianten orthogonal zum Typ. Alternativ können Invarianten erzwungen werden, indem Zusicherungen in Typen konvertiert werden . Um beispielsweise zu bestätigen, dass eine Datei vor dem Schließen geöffnet ist, kann File.open () einen OpenFile-Typ zurückgeben, der eine close () -Methode enthält, die in File nicht verfügbar ist. Eine Tic-Tac-Toe-API kann ein weiteres Beispiel für die Verwendung der Typisierung sein, um Invarianten zur Kompilierungszeit zu erzwingen. Das Typsystem kann sogar Turing-vollständig sein, z . B. Scala . Abhängig typisierte Sprachen und Theorembeweiser formalisieren die Modelle der Typisierung höherer Ordnung.
Aufgrund der Notwendigkeit, dass die Semantik über die Erweiterung abstrahiert , erwarte ich, dass die Verwendung der Typisierung zur Modellierung von Invarianten, dh der einheitlichen Denotationssemantik höherer Ordnung, dem Typestate überlegen ist. "Erweiterung" bezeichnet die unbegrenzte, permutierte Zusammensetzung einer unkoordinierten, modularen Entwicklung. Weil es für mich das Gegenteil von Vereinigung und damit Freiheitsgraden zu sein scheint, zwei voneinander abhängige Modelle (z. B. Typen und Typestate) zum Ausdrücken der gemeinsamen Semantik zu haben, die für eine erweiterbare Komposition nicht miteinander vereinheitlicht werden können . Beispielsweise wurde die Ausdrucksproblem- ähnliche Erweiterung in den Bereichen Subtypisierung, Funktionsüberladung und parametrische Typisierung vereinheitlicht.
Meine theoretische Position ist, dass es für das Vorhandensein von Wissen (siehe Abschnitt „Zentralisierung ist blind und nicht geeignet“) niemals ein allgemeines Modell geben wird, das eine 100% ige Abdeckung aller möglichen Invarianten in einer Turing-vollständigen Computersprache erzwingen kann. Damit Wissen existiert, gibt es viele unerwartete Möglichkeiten, dh Unordnung und Entropie müssen immer zunehmen. Dies ist die entropische Kraft. Um alle möglichen Berechnungen einer möglichen Erweiterung zu beweisen, müssen alle möglichen Erweiterungen a priori berechnet werden.
Aus diesem Grund existiert das Halting-Theorem, dh es ist unentscheidbar, ob jedes mögliche Programm in einer Turing-vollständigen Programmiersprache beendet wird. Es kann nachgewiesen werden, dass ein bestimmtes Programm beendet wird (eines, für das alle Möglichkeiten definiert und berechnet wurden). Es ist jedoch unmöglich zu beweisen, dass alle möglichen Erweiterungen dieses Programms beendet sind, es sei denn, die Möglichkeiten zur Erweiterung dieses Programms sind nicht vollständig (z. B. durch abhängige Eingabe). Da die Grundvoraussetzung für die Vollständigkeit von Turing eine unbegrenzte Rekursion ist , ist es intuitiv zu verstehen, wie Gödels Unvollständigkeitssätze und Russells Paradoxon auf die Erweiterung zutreffen.
Eine Interpretation dieser Theoreme bezieht sie in ein verallgemeinertes konzeptuelles Verständnis der entropischen Kraft ein:
quelle
Ich sehe in jeder Antwort Rechtecke und Quadrate und wie man den LSP verletzt.
Ich möchte anhand eines Beispiels aus der Praxis zeigen, wie der LSP angepasst werden kann:
Dieses Design entspricht dem LSP, da das Verhalten unabhängig von der von uns verwendeten Implementierung unverändert bleibt.
Und ja, Sie können LSP in dieser Konfiguration verletzen, indem Sie eine einfache Änderung wie folgt vornehmen:
Jetzt können die Untertypen nicht mehr auf die gleiche Weise verwendet werden, da sie nicht mehr das gleiche Ergebnis liefern.
quelle
Database::selectQuery
, nur die Teilmenge von SQL zu unterstützen, die von allen DB-Engines unterstützt wird. Das ist kaum praktikabel ... Trotzdem ist das Beispiel immer noch leichter zu verstehen als die meisten anderen, die hier verwendet werden.Es gibt eine Checkliste, um festzustellen, ob Sie gegen Liskov verstoßen oder nicht.
Checkliste:
Verlaufsbeschränkung : Wenn Sie eine Methode überschreiben, dürfen Sie eine nicht änderbare Eigenschaft in der Basisklasse nicht ändern. Werfen Sie einen Blick auf diesen Code und Sie können sehen, dass Name als nicht änderbar definiert ist (privater Satz), aber SubType führt eine neue Methode ein, mit der er geändert werden kann (durch Reflexion):
Es gibt zwei weitere Elemente: Kontravarianz von Methodenargumenten und Kovarianz von Rückgabetypen . Aber es ist in C # nicht möglich (ich bin ein C # -Entwickler), daher interessieren sie mich nicht.
Referenz:
quelle
Der LSP ist eine Regel über den Vertrag der Klassen: Wenn eine Basisklasse einen Vertrag erfüllt, müssen vom LSP abgeleitete Klassen auch diesen Vertrag erfüllen.
In Pseudo-Python
Erfüllt LSP, wenn jedes Mal, wenn Sie Foo für ein abgeleitetes Objekt aufrufen, genau die gleichen Ergebnisse erzielt werden wie beim Aufrufen von Foo für ein Basisobjekt, solange arg identisch ist.
quelle
2 + "2"
). Vielleicht verwechseln Sie "stark typisiert" mit "statisch typisiert"?Lange Rede kurzer Sinn , lassen wir Rechtecke Rechtecke und Quadrate Quadrate, praktisches Beispiel , wenn eine Elternklasse erstreckt, müssen Sie entweder KONSERVE die genaue Eltern API oder sie zu verlängern.
Angenommen , Sie haben ein Basis- ItemsRepository.
Und eine Unterklasse, die es erweitert:
Dann könnte ein Client mit der Base ItemsRepository-API arbeiten und sich darauf verlassen.
Der LSP ist fehlerhaft, wenn das Ersetzen der übergeordneten Klasse durch eine Unterklasse den Vertrag der API bricht .
Weitere Informationen zum Schreiben wartbarer Software finden Sie in meinem Kurs: https://www.udemy.com/enterprise-php/
quelle
Als ich zum ersten Mal über LSP las, ging ich davon aus, dass dies in einem sehr strengen Sinne gemeint war, was im Wesentlichen der Implementierung der Schnittstelle und dem typsicheren Casting gleichkam. Dies würde bedeuten, dass LSP entweder durch die Sprache selbst sichergestellt wird oder nicht. In diesem strengen Sinne ist ThreeDBoard beispielsweise für den Compiler sicherlich ein Ersatz für Board.
Nachdem ich mehr über das Konzept gelesen hatte, stellte ich fest, dass LSP im Allgemeinen breiter interpretiert wird.
Kurz gesagt, was es für Client-Code bedeutet, zu "wissen", dass das Objekt hinter dem Zeiger von einem abgeleiteten Typ ist und nicht vom Zeigertyp, ist nicht auf die Typensicherheit beschränkt. Die Einhaltung von LSP kann auch durch Prüfen des tatsächlichen Verhaltens des Objekts überprüft werden. Das heißt, Sie untersuchen die Auswirkung der Status- und Methodenargumente eines Objekts auf die Ergebnisse der Methodenaufrufe oder die Arten von Ausnahmen, die vom Objekt ausgelöst werden.
Wenn wir noch einmal auf das Beispiel zurückkommen, können die Board-Methoden theoretisch so gestaltet werden, dass sie auf ThreeDBoard einwandfrei funktionieren. In der Praxis wird es jedoch sehr schwierig sein, Verhaltensunterschiede zu vermeiden, mit denen der Client möglicherweise nicht richtig umgeht, ohne die Funktionalität zu beeinträchtigen, die ThreeDBoard hinzufügen soll.
Mit diesem Wissen kann die Bewertung der LSP-Einhaltung ein hervorragendes Instrument sein, um festzustellen, wann die Zusammensetzung der geeignetere Mechanismus für die Erweiterung vorhandener Funktionen ist, anstatt die Vererbung.
quelle
Ich denke, jeder hat irgendwie abgedeckt, was LSP technisch ist: Sie möchten im Grunde in der Lage sein, von Subtypdetails zu abstrahieren und Supertypen sicher zu verwenden.
Liskov hat also drei Regeln:
Signaturregel: Es sollte eine gültige Implementierung jeder Operation des Supertyps im Subtyp syntaktisch geben. Etwas, das ein Compiler für Sie überprüfen kann. Es gibt eine kleine Regel, weniger Ausnahmen auszulösen und mindestens so zugänglich zu sein wie die Supertyp-Methoden.
Methodenregel: Die Implementierung dieser Operationen ist semantisch einwandfrei.
Eigenschaftsregel: Dies geht über einzelne Funktionsaufrufe hinaus.
Alle diese Eigenschaften müssen beibehalten werden, und die zusätzliche Subtyp-Funktionalität sollte die Supertypeigenschaften nicht verletzen.
Wenn diese drei Dinge erledigt sind, haben Sie sich von den zugrunde liegenden Dingen entfernt und schreiben lose gekoppelten Code.
Quelle: Programmentwicklung in Java - Barbara Liskov
quelle
Ein wichtiges Beispiel für die Verwendung von LSP sind Softwaretests .
Wenn ich eine Klasse A habe, die eine LSP-kompatible Unterklasse von B ist, kann ich die Testsuite von B zum Testen von A wiederverwenden.
Um die Unterklasse A vollständig zu testen, muss ich wahrscheinlich einige weitere Testfälle hinzufügen, aber mindestens kann ich alle Testfälle der Oberklasse B wiederverwenden.
Eine Möglichkeit, dies zu realisieren, besteht darin, eine von McGregor als "Parallele Hierarchie zum Testen" bezeichnete Struktur aufzubauen: Meine
ATest
Klasse erbt vonBTest
. Dann ist eine Art Injektion erforderlich, um sicherzustellen, dass der Testfall mit Objekten vom Typ A und nicht vom Typ B funktioniert (ein einfaches Muster für die Vorlagenmethode reicht aus).Beachten Sie, dass die Wiederverwendung der Supertestsuite für alle Unterklassenimplementierungen tatsächlich eine Möglichkeit ist, zu testen, ob diese Unterklassenimplementierungen LSP-kompatibel sind. Man kann also auch argumentieren, dass man die Superklasse-Testsuite im Kontext einer beliebigen Unterklasse ausführen sollte .
Siehe auch die Antwort auf die Stackoverflow-Frage " Kann ich eine Reihe wiederverwendbarer Tests implementieren, um die Implementierung einer Schnittstelle zu testen? "
quelle
Lassen Sie uns in Java veranschaulichen:
Hier gibt es kein Problem, oder? Ein Auto ist definitiv ein Transportmittel, und hier können wir sehen, dass es die startEngine () -Methode seiner Oberklasse überschreibt.
Fügen wir ein weiteres Transportgerät hinzu:
Jetzt läuft nicht alles wie geplant! Ja, ein Fahrrad ist ein Transportgerät, hat jedoch keinen Motor und daher kann die Methode startEngine () nicht implementiert werden.
Die Lösung für diese Probleme ist eine korrekte Vererbungshierarchie, und in unserem Fall würden wir das Problem lösen, indem wir Klassen von Transportgeräten mit und ohne Motoren unterscheiden. Obwohl ein Fahrrad ein Transportmittel ist, hat es keinen Motor. In diesem Beispiel ist unsere Definition des Transportgeräts falsch. Es sollte keinen Motor haben.
Wir können unsere TransportationDevice-Klasse wie folgt umgestalten:
Jetzt können wir TransportationDevice für nicht motorisierte Geräte erweitern.
Und erweitern Sie TransportationDevice für motorisierte Geräte. Hier ist es besser, das Engine-Objekt hinzuzufügen.
Dadurch wird unsere Fahrzeugklasse spezialisierter, während das Liskov-Substitutionsprinzip eingehalten wird.
Und unsere Fahrradklasse entspricht auch dem Liskov-Substitutionsprinzip.
quelle
Diese Formulierung des LSP ist viel zu stark:
Was im Grunde bedeutet, dass S eine andere, vollständig gekapselte Implementierung genau derselben Sache wie T ist. Und ich könnte mutig sein und entscheiden, dass Leistung Teil des Verhaltens von P ist ...
Grundsätzlich verstößt jede Verwendung von Spätbindung gegen den LSP. Es ist der springende Punkt von OO, ein anderes Verhalten zu erzielen, wenn wir ein Objekt einer Art durch ein anderes Objekt ersetzen!
Die von Wikipedia zitierte Formulierung ist besser, da die Eigenschaft vom Kontext abhängt und nicht unbedingt das gesamte Verhalten des Programms umfasst.
quelle
In einem sehr einfachen Satz können wir sagen:
Die untergeordnete Klasse darf ihre Basisklassenmerkmale nicht verletzen. Es muss damit fähig sein. Wir können sagen, es ist dasselbe wie Subtypisierung.
quelle
Beispiel:
Nachfolgend finden Sie das klassische Beispiel, für das das Substitutionsprinzip von Liskov verletzt wird. Im Beispiel werden 2 Klassen verwendet: Rechteck und Quadrat. Nehmen wir an, dass das Rectangle-Objekt irgendwo in der Anwendung verwendet wird. Wir erweitern die Anwendung und fügen die Square-Klasse hinzu. Die quadratische Klasse wird unter bestimmten Bedingungen von einem Factory-Muster zurückgegeben, und wir wissen nicht genau, welcher Objekttyp zurückgegeben wird. Aber wir wissen, dass es ein Rechteck ist. Wir erhalten das Rechteckobjekt, setzen die Breite auf 5 und die Höhe auf 10 und erhalten die Fläche. Für ein Rechteck mit der Breite 5 und der Höhe 10 sollte die Fläche 50 betragen. Stattdessen beträgt das Ergebnis 100
Siehe auch: Open Close-Prinzip
Einige ähnliche Konzepte für eine bessere Struktur: Konvention über Konfiguration
quelle
Das Liskov-Substitutionsprinzip
quelle
Ein Nachtrag:
Ich frage mich, warum niemand über die Invarianten, Voraussetzungen und Post-Bedingungen der Basisklasse geschrieben hat, die von den abgeleiteten Klassen eingehalten werden müssen. Damit eine abgeleitete Klasse D von der Basisklasse B vollständig unterstützt werden kann, muss Klasse D bestimmte Bedingungen erfüllen:
Der Abgeleitete muss sich also der drei oben genannten Bedingungen bewusst sein, die von der Basisklasse auferlegt werden. Daher sind die Regeln für die Untertypisierung im Voraus festgelegt. Dies bedeutet, dass die Beziehung "IS A" nur eingehalten werden darf, wenn bestimmte Regeln vom Subtyp eingehalten werden. Diese Regeln in Form von Invarianten, Vorkodierungen und Nachbedingungen sollten durch einen formellen „ Entwurfsvertrag “ festgelegt werden.
Weitere Diskussionen dazu finden Sie in meinem Blog: Liskov-Substitutionsprinzip
quelle
Der LSP besagt in einfachen Worten , dass Objekte derselben Oberklasse miteinander ausgetauscht werden können sollten, ohne etwas zu beschädigen .
Zum Beispiel, wenn wir eine haben
Cat
und eineDog
Klasse von einer abgeleitetenAnimal
Klasse sollten alle Funktionen die Klasse Tier mit der Lage sein , zu verwendenCat
oderDog
normal und verhalten.quelle
Wäre die Implementierung von ThreeDBoard in Bezug auf eine Reihe von Boards so nützlich?
Vielleicht möchten Sie ThreeDBoard-Scheiben in verschiedenen Ebenen als Board behandeln. In diesem Fall möchten Sie möglicherweise eine Schnittstelle (oder eine abstrakte Klasse) für Board abstrahieren, um mehrere Implementierungen zu ermöglichen.
In Bezug auf die externe Schnittstelle möchten Sie möglicherweise eine Board-Schnittstelle für TwoDBoard und ThreeDBoard herausrechnen (obwohl keine der oben genannten Methoden passt).
quelle
Ein Quadrat ist ein Rechteck, bei dem die Breite der Höhe entspricht. Wenn das Quadrat zwei verschiedene Größen für die Breite und Höhe festlegt, verletzt es die quadratische Invariante. Dies wird durch die Einführung von Nebenwirkungen umgangen. Aber wenn das Rechteck eine setSize (Höhe, Breite) mit der Voraussetzung 0 <Höhe und 0 <Breite hatte. Die abgeleitete Subtypmethode erfordert height == width; eine stärkere Voraussetzung (und das verstößt gegen lsp). Dies zeigt, dass Quadrat zwar ein Rechteck ist, aber kein gültiger Untertyp, da die Vorbedingung verstärkt ist. Die Umgehung (im Allgemeinen eine schlechte Sache) verursacht eine Nebenwirkung und dies schwächt die Post-Bedingung (die lsp verletzt). setWidth auf der Basis hat die Postbedingung 0 <width. Das Abgeleitte schwächt es mit Höhe == Breite.
Daher ist ein Quadrat mit veränderbarer Größe kein Rechteck mit veränderbarer Größe.
quelle
Dieses Prinzip wurde von Barbara Liskov eingeführt 1987 und erweitert das Open-Closed-Prinzip, indem es sich auf das Verhalten einer Oberklasse und ihre Subtypen konzentriert.
Ihre Bedeutung wird deutlich, wenn wir die Konsequenzen einer Verletzung betrachten. Stellen Sie sich eine Anwendung vor, die die folgende Klasse verwendet.
Stellen Sie sich vor, der Kunde verlangt eines Tages die Möglichkeit, neben Rechtecken auch Quadrate zu bearbeiten. Da ein Quadrat ein Rechteck ist, sollte die Quadratklasse von der Rechteckklasse abgeleitet werden.
Auf diese Weise stoßen wir jedoch auf zwei Probleme:
Ein Quadrat benötigt nicht sowohl Höhen- als auch Breitenvariablen, die vom Rechteck geerbt werden. Dies kann zu einer erheblichen Verschwendung von Speicher führen, wenn Hunderttausende von Quadratobjekten erstellt werden müssen. Die vom Rechteck geerbten Eigenschaften des Setzers für Breite und Höhe sind für ein Quadrat ungeeignet, da Breite und Höhe eines Quadrats identisch sind. Um sowohl Höhe als auch Breite auf den gleichen Wert festzulegen, können Sie zwei neue Eigenschaften wie folgt erstellen:
Wenn nun jemand die Breite eines quadratischen Objekts festlegt, ändert sich seine Höhe entsprechend und umgekehrt.
Gehen wir weiter und betrachten diese andere Funktion:
Wenn wir dieser Funktion einen Verweis auf ein quadratisches Objekt übergeben, verletzen wir den LSP, da die Funktion für Ableitungen ihrer Argumente nicht funktioniert. Die Eigenschaften width und height sind nicht polymorph, da sie im Rechteck nicht als virtuell deklariert sind (das quadratische Objekt wird beschädigt, da die Höhe nicht geändert wird).
Wenn wir jedoch die Setter-Eigenschaften als virtuell deklarieren, werden wir einem weiteren Verstoß ausgesetzt sein, dem OCP. Tatsächlich führt die Erstellung eines abgeleiteten Klassenquadrats zu Änderungen am Basisklassenrechteck.
quelle
Die klarste Erklärung für LSP, die ich bisher gefunden habe, war: "Das Liskov-Substitutionsprinzip besagt, dass das Objekt einer abgeleiteten Klasse ein Objekt der Basisklasse ersetzen kann, ohne Fehler im System zu verursachen oder das Verhalten der Basisklasse zu ändern "von hier . Der Artikel enthält ein Codebeispiel für die Verletzung und Behebung von LSP.
quelle
Angenommen, wir verwenden ein Rechteck in unserem Code
In unserer Geometrieklasse haben wir gelernt, dass ein Quadrat eine spezielle Art von Rechteck ist, da seine Breite der Länge seiner Höhe entspricht. Lassen Sie uns auch eine
Square
Klasse basierend auf diesen Informationen erstellen:Wenn wir das Ersetzen
Rectangle
mitSquare
in unserem ersten Code, dann wird es brechen:Dies liegt daran, dass der
Square
eine neue Voraussetzung hat, die wir in derRectangle
Klasse nicht hatten :width == height
. Laut LSP sollten dieRectangle
Instanzen durchRectangle
Instanzen der Unterklasse ersetzt werden können. Dies liegt daran, dass diese Instanzen die Typprüfung fürRectangle
Instanzen bestehen und daher unerwartete Fehler in Ihrem Code verursachen.Dies war ein Beispiel für den Teil "Voraussetzungen können in einem Subtyp nicht verstärkt werden" im Wiki-Artikel . Zusammenfassend lässt sich sagen, dass ein Verstoß gegen LSP wahrscheinlich irgendwann zu Fehlern in Ihrem Code führt.
quelle
LSP sagt, dass Objekte durch ihre Untertypen ersetzt werden können. Andererseits weist dieses Prinzip auf
Das folgende Beispiel hilft dabei, LSP besser zu verstehen.
Ohne LSP:
Fixierung durch LSP:
quelle
Ich ermutige Sie, den Artikel zu lesen: Verstoß gegen das Liskov-Substitutionsprinzip (LSP) .
Dort finden Sie eine Erklärung zum Liskov-Substitutionsprinzip, allgemeine Hinweise, anhand derer Sie erraten können, ob Sie bereits gegen das Liskov-Substitutionsprinzip verstoßen haben, und ein Beispiel für einen Ansatz, mit dem Sie Ihre Klassenhierarchie sicherer machen können.
quelle
Das LISKOV SUBSTITUTION PRINCIPLE (aus dem Buch von Mark Seemann) besagt, dass wir in der Lage sein sollten, eine Implementierung einer Schnittstelle durch eine andere zu ersetzen, ohne den Client oder die Implementierung zu beschädigen. Dieses Prinzip ermöglicht es, Anforderungen zu erfüllen, die in der Zukunft auftreten, selbst wenn wir können. ' Ich sehe sie heute nicht voraus.
Wenn wir den Computer von der Wand trennen (Implementierung), fallen weder die Steckdose (Schnittstelle) noch der Computer (Client) aus (wenn es sich um einen Laptop handelt, kann er sogar einige Zeit mit seinen Batterien betrieben werden). . Bei Software erwartet ein Client jedoch häufig, dass ein Dienst verfügbar ist. Wenn der Dienst entfernt wurde, erhalten wir eine NullReferenceException. Um mit dieser Art von Situation fertig zu werden, können wir eine Implementierung einer Schnittstelle erstellen, die „nichts“ tut. Dies ist ein Entwurfsmuster, das als Null-Objekt bekannt ist [4] und ungefähr dem Herausziehen des Computers von der Wand entspricht. Da wir lose Kopplung verwenden, können wir eine echte Implementierung durch etwas ersetzen, das nichts tut, ohne Probleme zu verursachen.
quelle
Das Substitutionsprinzip von Likov besagt, dass, wenn ein Programmmodul eine Basisklasse verwendet , der Verweis auf die Basisklasse durch eine abgeleitete Klasse ersetzt werden kann, ohne die Funktionalität des Programmmoduls zu beeinträchtigen.
Absicht - Abgeleitete Typen müssen ihre Basistypen vollständig ersetzen können.
Beispiel - Co-Varianten-Rückgabetypen in Java.
quelle
Hier ist ein Auszug aus diesem Beitrag , der die Dinge gut verdeutlicht:
[..] Um einige Prinzipien zu verstehen, ist es wichtig zu erkennen, wann gegen sie verstoßen wurde. Das werde ich jetzt tun.
Was bedeutet die Verletzung dieses Prinzips? Dies impliziert, dass ein Objekt den Vertrag nicht erfüllt, der durch eine mit einer Schnittstelle ausgedrückte Abstraktion auferlegt wird. Mit anderen Worten bedeutet dies, dass Sie Ihre Abstraktionen falsch identifiziert haben.
Betrachten Sie das folgende Beispiel:
Ist das eine Verletzung von LSP? Ja. Dies liegt daran, dass der Vertrag des Kontos besagt, dass ein Konto zurückgezogen werden würde, dies ist jedoch nicht immer der Fall. Was soll ich also tun, um das Problem zu beheben? Ich ändere nur den Vertrag:
Voilà, jetzt ist der Vertrag erfüllt.
Diese subtile Verletzung zwingt einen Kunden häufig dazu, den Unterschied zwischen den verwendeten konkreten Objekten zu erkennen. In Anbetracht des Vertrags des ersten Kontos könnte dies beispielsweise folgendermaßen aussehen:
Und dies verstößt automatisch gegen das Open-Closed-Prinzip [dh für die Geldabhebungspflicht. Weil Sie nie wissen, was passiert, wenn ein Objekt, das gegen den Vertrag verstößt, nicht genug Geld hat. Wahrscheinlich gibt es nur nichts zurück, wahrscheinlich wird eine Ausnahme ausgelöst. Sie müssen also prüfen, ob dies der Fall ist
hasEnoughMoney()
nicht Teil einer Schnittstelle ist. Diese erzwungene konkretklassenabhängige Prüfung ist also eine OCP-Verletzung.Dieser Punkt befasst sich auch mit einem Missverständnis, das mir bei LSP-Verstößen häufig begegnet. Es heißt: "Wenn sich das Verhalten eines Elternteils bei einem Kind geändert hat, verstößt es gegen LSP." Dies ist jedoch nicht der Fall - solange ein Kind nicht gegen den Vertrag seiner Eltern verstößt.
quelle