Ich versuche, die SOLID-Prinzipien von OOP zu verstehen, und bin zu dem Schluss gekommen, dass LSP und OCP einige Ähnlichkeiten aufweisen (um nicht mehr zu sagen).
Das Open / Closed-Prinzip besagt, dass "Software-Entitäten (Klassen, Module, Funktionen usw.) für Erweiterungen offen, für Änderungen jedoch geschlossen sein sollten".
LSP gibt in einfachen Worten an, dass jede Instanz von Foo
durch eine Instanz von ersetzt werden kann, von Bar
der abgeleitet wird, Foo
und dass das Programm genauso funktioniert.
Ich bin kein Pro-OOP-Programmierer, aber es scheint mir, dass LSP nur möglich ist, wenn Bar
, abgeleitet von Foo
, nichts daran geändert, sondern nur erweitert wird. Dies bedeutet, dass insbesondere Programm-LSP nur dann wahr ist, wenn OCP wahr ist, und OCP nur dann, wenn LSP wahr ist. Das bedeutet, dass sie gleich sind.
Korrigiere mich, wenn ich falsch liege. Ich möchte diese Ideen wirklich verstehen. Vielen Dank für eine Antwort.
Square
vonRectangle
LSP nicht. (Aber es ist wahrscheinlich immer noch schlechtes Design in dem unveränderlichen Fall, da Sie Quadrate haben könnenRectangle
, die keine sind,Square
die nicht mit Mathematik übereinstimmen)Antworten:
Meine Güte, es gibt einige seltsame Missverständnisse darüber, was OCP und LSP sind, und einige sind darauf zurückzuführen, dass einige Terminologien und verwirrende Beispiele nicht übereinstimmen. Beide Prinzipien sind nur dann "dasselbe", wenn Sie sie auf die gleiche Weise implementieren. Muster folgen normalerweise den Prinzipien auf die eine oder andere Weise, mit wenigen Ausnahmen.
Die Unterschiede werden weiter unten erläutert, aber lassen Sie uns zunächst einen Blick auf die Prinzipien selbst werfen:
Open-Closed-Prinzip (OCP)
Laut Onkel Bob :
Beachten Sie, dass das Wort " Erweitern" in diesem Fall nicht unbedingt bedeutet, dass Sie die tatsächliche Klasse, die das neue Verhalten benötigt, in eine Unterklasse unterteilen sollten. Sehen Sie, wie ich bei der ersten Nichtübereinstimmung der Terminologie erwähnt habe? Das Schlüsselwort
extend
bedeutet nur Unterklassen in Java, die Prinzipien sind jedoch älter als in Java.Das Original stammt von Bertrand Meyer aus dem Jahr 1988:
Hier ist es viel klarer, dass das Prinzip auf Software-Entitäten angewendet wird . Ein schlechtes Beispiel wäre, die Software-Entität zu überschreiben, da Sie den Code vollständig ändern, anstatt einen Erweiterungspunkt bereitzustellen. Das Verhalten der Software-Entität selbst sollte erweiterbar sein und ein gutes Beispiel dafür ist die Implementierung des Strategie-Musters (weil es meiner Meinung nach am einfachsten ist, das GoF-Pattern-Bündel zu zeigen):
Im obigen Beispiel
Context
ist das für weitere Änderungen gesperrt . Die meisten Programmierer möchten die Klasse wahrscheinlich in Unterklassen unterteilen, um sie zu erweitern. Dies ist jedoch nicht der Fall, da davon ausgegangen wird, dass das Verhalten durch alles, was die Schnittstelle implementiert , geändert werden kannIBehavior
.Dh die Kontextklasse ist zur Änderung geschlossen, aber zur Erweiterung offen . Es folgt tatsächlich einem anderen Grundprinzip, da wir das Verhalten mit der Objektzusammensetzung anstelle der Vererbung setzen:
Ich werde den Leser über dieses Prinzip informieren, da es außerhalb des Rahmens dieser Frage liegt. Nehmen wir an, wir haben die folgenden Implementierungen der IBehavior-Schnittstelle, um mit dem Beispiel fortzufahren:
Mithilfe dieses Musters können wir das Verhalten des Kontexts zur Laufzeit über die
setBehavior
Methode als Erweiterungspunkt ändern .Wenn Sie also die "geschlossene" Kontextklasse erweitern möchten, führen Sie dies durch, indem Sie ihre "offene" Abhängigkeit von der Zusammenarbeit in Unterklassen unterteilen. Dies ist eindeutig nicht dasselbe wie das Unterteilen des Kontexts selbst, es ist jedoch OCP. LSP erwähnt dies ebenfalls nicht.
Erweitern mit Mixins statt Vererbung
Es gibt andere Möglichkeiten, OCP durchzuführen als Unterklassen. Eine Möglichkeit besteht darin, Ihre Klassen durch die Verwendung von Mixins für eine Erweiterung offen zu halten . Dies ist z. B. in Sprachen nützlich, die eher prototypbasiert als klassenbasiert sind. Die Idee ist, ein dynamisches Objekt mit mehr Methoden oder Attributen nach Bedarf zu ändern, dh Objekte, die mit anderen Objekten gemischt oder "eingemischt" werden.
Hier ist ein Javascript-Beispiel für ein Mixin, das eine einfache HTML-Vorlage für Anker rendert:
Die Objekte sollen dynamisch erweitert werden. Dies hat den Vorteil, dass Objekte Methoden gemeinsam nutzen können, auch wenn sie sich in völlig unterschiedlichen Domänen befinden. In dem obigen Fall können Sie leicht andere Arten von HTML-Ankern erstellen, indem Sie Ihre spezifische Implementierung mit der erweitern
LinkMixin
.In Bezug auf OCP sind die "Mixins" Erweiterungen. Im obigen Beispiel
YoutubeLink
ist das unsere Software-Entität, die für Änderungen geschlossen, aber für Erweiterungen durch die Verwendung von Mixins geöffnet ist. Die Objekthierarchie ist abgeflacht, wodurch es unmöglich ist, nach Typen zu suchen. Dies ist jedoch keine schlechte Sache, und ich werde weiter unten erläutern, dass das Prüfen auf Typen im Allgemeinen eine schlechte Idee ist und die Idee mit Polymorphismus bricht.Beachten Sie, dass es mit dieser Methode möglich ist,
extend
mehrere Objekte zu vererben, da die meisten Implementierungen mehrere Objekte einmischen können:Das einzige, was Sie beachten müssen, ist, die Namen nicht zu kollidieren, dh Mixins definieren zufällig den gleichen Namen einiger Attribute oder Methoden, wie sie überschrieben werden. Nach meiner bescheidenen Erfahrung ist dies kein Problem, und wenn es doch passiert, ist es ein Hinweis auf ein fehlerhaftes Design.
Liskovs Substitutionsprinzip (LSP)
Onkel Bob definiert es einfach durch:
Dieses Prinzip ist alt, in der Tat unterscheidet die Definition von Onkel Bob die Prinzipien nicht, da dadurch LSP immer noch eng mit OCP verwandt ist, da im obigen Strategiebeispiel derselbe Supertyp verwendet wird (
IBehavior
). Schauen wir uns also die ursprüngliche Definition von Barbara Liskov an und sehen wir, ob wir noch etwas über dieses Prinzip herausfinden können, das wie ein mathematischer Satz aussieht:Lasst uns eine Weile mit den Schultern zucken, denn es werden überhaupt keine Klassen erwähnt. In JavaScript können Sie LSP tatsächlich folgen, obwohl es nicht explizit klassenbasiert ist. Wenn Ihr Programm eine Liste von mindestens ein paar JavaScript-Objekten enthält, die:
... dann haben die Objekte den gleichen "Typ" und es ist für das Programm nicht wirklich wichtig. Dies ist im Wesentlichen Polymorphismus . Im allgemeinen Sinne; Sie sollten den tatsächlichen Untertyp nicht kennen müssen, wenn Sie dessen Schnittstelle verwenden. OCP sagt dazu nichts explizites. Es zeigt auch tatsächlich einen Designfehler auf, den die meisten unerfahrenen Programmierer machen:
Wann immer Sie den Drang verspüren, den Untertyp eines Objekts zu überprüfen, tun Sie dies höchstwahrscheinlich FALSCH.
Okay, es könnte also nicht immer falsch sein, aber wenn Sie den Drang haben, eine Typprüfung mit
instanceof
oder Aufzählungen durchzuführen, ist das Programm möglicherweise etwas komplizierter, als es sein muss. Dies ist jedoch nicht immer der Fall; Schnelle und schmutzige Hacks, um die Dinge zum Laufen zu bringen, sind in meinen Augen eine gute Konzession, wenn die Lösung klein genug ist und wenn Sie gnadenloses Refactoring praktizieren , wird sie möglicherweise verbessert, sobald Änderungen dies erfordern.Abhängig vom eigentlichen Problem gibt es verschiedene Möglichkeiten, um diesen "Designfehler" zu umgehen:
Beides sind gängige Code-Design- "Fehler". Sie können verschiedene Refactorings durchführen, z. B. die Pull-up-Methode oder die Refactor-Funktion für ein Muster wie das Visitor-Muster .
Ich mag das Besuchermuster sehr, da es für große if-Statement-Spaghetti geeignet ist und einfacher zu implementieren ist, als Sie es für vorhandenen Code halten würden. Angenommen, wir haben den folgenden Kontext:
Die Ergebnisse der if-Anweisung können in ihre eigenen Besucher übersetzt werden, da jede von einer Entscheidung und einem auszuführenden Code abhängt. Wir können diese wie folgt extrahieren:
Wenn der Programmierer zu diesem Zeitpunkt nichts über das Besuchermuster wusste, implementierte er stattdessen die Context-Klasse, um zu überprüfen, ob es sich um einen bestimmten Typ handelt. Da die Visitor-Klassen über eine boolesche
canDo
Methode verfügen , kann der Implementierer mithilfe dieses Methodenaufrufs feststellen, ob es sich um das richtige Objekt für die Ausführung der Aufgabe handelt. Die Kontextklasse kann alle Besucher wie folgt verwenden (und neue hinzufügen):Beide Muster folgen OCP und LSP, zeigen jedoch beide unterschiedliche Dinge an. Wie sieht Code aus, wenn er gegen eines der Prinzipien verstößt?
Ein Prinzip verletzen, aber dem anderen folgen
Es gibt Möglichkeiten, eines der Prinzipien zu brechen, aber das andere muss befolgt werden. Die folgenden Beispiele scheinen aus gutem Grund erfunden zu sein, aber ich habe tatsächlich gesehen, dass sie im Produktionscode auftauchen (und sogar schlechter):
Folgt OCP, aber nicht LSP
Nehmen wir an, wir haben den angegebenen Code:
Dieser Code folgt dem Open-Closed-Prinzip. Wenn wir die
GetPersons
Methode des Kontexts aufrufen , erhalten wir eine Reihe von Personen, die alle ihre eigenen Implementierungen haben. Das bedeutet, dass IPerson für Änderungen geschlossen, aber für Erweiterungen geöffnet ist. Es wird jedoch dunkel, wenn wir es benutzen müssen:Sie müssen Typüberprüfung und Typkonvertierung durchführen! Weißt du noch, wie ich oben erwähnt habe, wie schlecht die Typenprüfung ist ? Ach nein! Aber keine Angst, wie bereits erwähnt, führen Sie entweder ein Pull-up-Refactoring durch oder implementieren Sie ein Besuchermuster. In diesem Fall können wir einfach ein Pull-Up-Refactoring durchführen, nachdem wir eine allgemeine Methode hinzugefügt haben:
Der Vorteil ist nun, dass Sie nach LSP nicht mehr den genauen Typ kennen müssen:
Folgt LSP, aber nicht OCP
Schauen wir uns einen Code an, der auf LSP folgt, aber nicht auf OCP. Er ist ein bisschen erfunden, aber bei mir ist es ein sehr subtiler Fehler:
Der Code führt LSP aus, da der Kontext LiskovBase verwenden kann, ohne den tatsächlichen Typ zu kennen. Sie würden denken, dieser Code folgt auch OCP, aber schauen Sie genau hin, ist die Klasse wirklich geschlossen ? Was wäre, wenn die
doStuff
Methode mehr als nur eine Zeile ausdrucken würde?Die Antwort auf OCP lautet einfach: NEIN , das liegt nicht daran, dass wir in diesem Objektdesign den Code vollständig durch etwas anderes überschreiben müssen. Dies öffnet die Möglichkeit zum Ausschneiden und Einfügen von Würmern, da Sie Code aus der Basisklasse kopieren müssen, um die Arbeit zu starten. Die
doStuff
Methode kann zwar erweitert werden, wurde jedoch für Änderungen nicht vollständig geschlossen.Darauf können wir das Template-Methodenmuster anwenden . Das Template-Methodenmuster ist in Frameworks so verbreitet, dass Sie es möglicherweise verwendet haben, ohne es zu wissen (z. B. Java-Swing-Komponenten, C # -Formulare und -Komponenten usw.). Hier ist eine Möglichkeit, die
doStuff
Änderungsmethode zu schließen und sicherzustellen, dass sie geschlossen bleibt, indem Sie sie mit demfinal
Schlüsselwort von Java markieren . Dieses Schlüsselwort verhindert, dass die Klasse weiter in Unterklassen unterteilt wird (in C # können Siesealed
dasselbe tun).Dieses Beispiel folgt OCP und scheint albern, was es ist, aber stellen Sie sich dies mit mehr Code skaliert zu behandeln. Ich sehe immer wieder, wie Code in der Produktion bereitgestellt wird, wobei Unterklassen alles außer Kraft setzen und der überschriebene Code meist zwischen den Implementierungen eingefügt wird. Es funktioniert, aber wie bei allen Code-Duplikaten ist es auch ein Setup für Wartungs-Albträume.
Fazit
Ich hoffe, dies alles klärt einige Fragen in Bezug auf OCP und LSP und die Unterschiede / Ähnlichkeiten zwischen ihnen. Es ist einfach, sie als gleich zu entlassen, aber die obigen Beispiele sollten zeigen, dass sie nicht so sind.
Beachten Sie, dass aus dem obigen Beispielcode Folgendes hervorgeht:
Bei OCP geht es darum, den Arbeitscode zu sperren, ihn aber mit einigen Erweiterungspunkten offen zu halten.
Dies dient zur Vermeidung von Codeduplizierungen, indem der Code eingekapselt wird, der sich wie im Beispiel des Musters der Vorlagenmethode ändert. Es kann auch schnell scheitern, da das Brechen von Änderungen schmerzhaft ist (dh eine Stelle ändern, es überall anders brechen). Aus Wartungsgründen ist das Konzept der Kapselung von Änderungen eine gute Sache, da Änderungen immer vorkommen .
Bei LSP geht es darum, den Benutzer mit verschiedenen Objekten zu beauftragen, die einen Supertyp implementieren, ohne den tatsächlichen Typ zu überprüfen. Darum geht es inhärent beim Polymorphismus .
Dieses Prinzip bietet eine Alternative zur Typprüfung und -konvertierung, die mit zunehmender Anzahl von Typen außer Kontrolle geraten kann und durch Pull-up-Refactoring oder das Anwenden von Mustern wie z. B. Visitor erreicht werden kann.
quelle
Dies ist etwas, das viel Verwirrung stiftet. Ich ziehe es vor, diese Prinzipien etwas philosophisch zu betrachten, weil es viele verschiedene Beispiele dafür gibt, und manchmal erfassen konkrete Beispiele nicht wirklich ihr gesamtes Wesen.
Was OCP versucht zu beheben
Angenommen, wir müssen einem bestimmten Programm Funktionen hinzufügen. Der einfachste Weg, dies zu tun, besteht insbesondere für Personen, die geschult wurden, prozedural zu denken, darin, eine if-Klausel hinzuzufügen, wo immer dies erforderlich ist, oder etwas Ähnliches.
Die Probleme damit sind
Sie können dies tun, indem Sie ein zusätzliches Feld zu allen Büchern mit dem Namen "is_on_sale" hinzufügen. Anschließend können Sie dieses Feld beim Drucken eines Buchpreises aktivieren. Alternativ können Sie verkaufte Bücher aus der Datenbank mit einem anderen Typ instanziieren, der gedruckt wird "(ON SALE)" in der Preiskette (kein perfektes Design, aber es liefert den Punkt nach Hause).
Das Problem bei der ersten prozeduralen Lösung ist ein zusätzliches Feld für jedes Buch und in vielen Fällen eine zusätzliche redundante Komplexität. Die zweite Lösung erzwingt Logik nur dort, wo sie tatsächlich benötigt wird.
Bedenken Sie nun, dass in vielen Fällen unterschiedliche Daten und Logik erforderlich sein können, und Sie werden sehen, warum es eine gute Idee ist, beim Entwerfen Ihrer Klassen auf OCP zu achten oder auf geänderte Anforderungen zu reagieren.
Inzwischen sollten Sie die Hauptidee haben: Versuchen Sie, sich in eine Situation zu versetzen, in der neuer Code als polymorphe Erweiterungen und nicht als prozedurale Änderungen implementiert werden kann.
Aber keine Angst davor haben, den Kontext zu analysieren und herauszufinden, ob die Nachteile die Vorteile überwiegen, denn selbst ein Prinzip wie OCP kann ein 20-Klassen-Chaos aus einem 20-Zeilen-Programm machen, wenn es nicht sorgfältig behandelt wird .
Was LSP versucht zu beheben
Wir alle lieben die Wiederverwendung von Code. Eine Krankheit, die folgt, ist, dass viele Programme es nicht vollständig verstehen, bis zu dem Punkt, an dem sie gebräuchliche Codezeilen blind faktorisieren, nur um unlesbare Komplexität und redundante enge Kopplung zwischen Modulen zu erzeugen, die, abgesehen von ein paar Codezeilen, nichts gemeinsam haben, was die konzeptionelle Arbeit betrifft, die zu erledigen ist.
Das größte Beispiel hierfür ist die Wiederverwendung von Schnittstellen . Sie haben es wahrscheinlich selbst gesehen. Eine Klasse implementiert eine Schnittstelle nicht, weil sie eine logische Implementierung ist (oder eine Erweiterung bei konkreten Basisklassen), sondern weil die Methoden, die sie zu diesem Zeitpunkt deklariert, für sie die richtigen Signaturen haben.
Aber dann stößt du auf ein Problem. Wenn Klassen Schnittstellen nur unter Berücksichtigung der Signaturen der von ihnen deklarierten Methoden implementieren, können Sie Instanzen von Klassen von einer konzeptionellen Funktionalität an Stellen übergeben, die völlig andere Funktionen erfordern, die nur von ähnlichen Signaturen abhängen.
Das ist nicht so schrecklich, aber es sorgt für viel Verwirrung und wir haben die Technologie, um zu verhindern, dass wir solche Fehler machen. Wir müssen Schnittstellen als API + -Protokoll behandeln . Die API wird in Deklarationen angezeigt, und das Protokoll wird in vorhandenen Verwendungen der Schnittstelle angezeigt. Wenn wir zwei konzeptionelle Protokolle haben, die dieselbe API verwenden, sollten sie als zwei verschiedene Schnittstellen dargestellt werden. Andernfalls geraten wir in den DRY-Dogmatismus und erschaffen ironischerweise nur schwer zu pflegenden Code.
Jetzt sollten Sie in der Lage sein, die Definition perfekt zu verstehen. LSP sagt: Vererben Sie nicht von einer Basisklasse und implementieren Sie Funktionen in den Unterklassen, mit denen andere Orte, die von der Basisklasse abhängen, nicht auskommen.
quelle
Meinem Verständnis nach:
OCP sagt: "Wenn Sie neue Funktionen hinzufügen möchten, erstellen Sie eine neue Klasse, die eine vorhandene erweitert, anstatt sie zu ändern."
LSP sagt: "Wenn Sie eine neue Klasse erstellen, die eine vorhandene Klasse erweitert, stellen Sie sicher, dass diese vollständig mit ihrer Basis austauschbar ist."
Ich denke, sie ergänzen sich, aber sie sind nicht gleich.
quelle
Während es stimmt, dass OCP und LSP beide mit Modifikation zu tun haben, ist die Art der Modifikation, von der OCP spricht, nicht die, von der LSP spricht.
Das Ändern in Bezug auf OCP ist die physische Aktion eines Entwicklers , der Code in eine vorhandene Klasse schreibt .
LSP behandelt die Verhaltensänderung, die eine abgeleitete Klasse im Vergleich zu ihrer Basisklasse mit sich bringt, und die Laufzeitänderung der Programmausführung, die durch die Verwendung der Unterklasse anstelle der Superklasse verursacht werden kann.
Obwohl sie aus der Ferne ähnlich aussehen könnten, ist OCP! = LSP. Tatsächlich denke ich, dass dies die einzigen 2 SOLID-Prinzipien sind, die nicht in Bezug aufeinander verstanden werden können.
quelle
Das ist falsch. LSP gibt an, dass die Klassenleiste kein Verhalten einführen sollte, das nicht erwartet wird, wenn der Code Foo verwendet, wenn die Leiste von Foo abgeleitet ist. Es hat nichts mit Funktionsverlust zu tun. Sie können Funktionen entfernen, aber nur, wenn Code, der Foo verwendet, nicht von dieser Funktionalität abhängt.
Letztendlich ist dies jedoch in der Regel schwer zu erreichen, da Code, der Foo verwendet, die meiste Zeit von seinem Verhalten abhängt. Das Entfernen verstößt also gegen LSP. Eine solche Vereinfachung ist jedoch nur ein Teil von LSP.
quelle
Über Objekte, die möglicherweise verletzen
Um den Unterschied zu verstehen, sollten Sie die Themen beider Prinzipien verstehen. Es ist kein abstrakter Teil des Codes oder der Situation, der oder die gegen einen Grundsatz verstößt. Es ist immer eine bestimmte Komponente - Funktion, Klasse oder ein Modul -, die gegen OCP oder LSP verstoßen kann.
Wer kann gegen LSP verstoßen
Man kann nur dann prüfen, ob der LSP defekt ist, wenn es eine Schnittstelle mit einem Vertrag und eine Implementierung dieser Schnittstelle gibt. Wenn die Implementierung nicht mit der Schnittstelle oder allgemein mit dem Vertrag übereinstimmt, ist der LSP defekt.
Einfachstes Beispiel:
Der Vertrag
addObject
sieht eindeutig vor, dass das Argument an den Container angehängt werden muss. UndCustomContainer
bricht diesen Vertrag eindeutig. SomitCustomContainer.addObject
verletzt die Funktion LSP. SomitCustomContainer
verletzt die Klasse LSP. Die wichtigste Konsequenz ist, dassCustomContainer
nicht weitergegeben werden kannfillWithRandomNumbers()
.Container
kann nicht durch ersetzt werdenCustomContainer
.Denken Sie an einen sehr wichtigen Punkt. Es ist nicht dieser ganze Code, der LSP bricht, es ist speziell
CustomContainer.addObject
und allgemeinCustomContainer
, der LSP bricht. Wenn Sie angeben, dass gegen LSP verstoßen wird, sollten Sie immer zwei Dinge angeben:Das ist es. Nur ein Vertrag und dessen Umsetzung. Ein Downcast im Code sagt nichts über eine LSP-Verletzung aus.
Wer kann OCP verletzen
Man kann nur dann prüfen, ob OCP verletzt wird, wenn es einen begrenzten Datensatz und eine Komponente gibt, die Werte aus diesem Datensatz verarbeitet. Wenn sich die Grenzen des Datensatzes im Laufe der Zeit ändern und dies eine Änderung des Quellcodes der Komponente erfordert, verstößt die Komponente gegen OCP.
Hört sich komplex an. Versuchen wir ein einfaches Beispiel:
Der Datensatz ist die Menge der unterstützten Plattformen.
PlatformDescriber
ist die Komponente, die Werte aus diesem Datensatz verarbeitet. Das Hinzufügen einer neuen Plattform erfordert die Aktualisierung des Quellcodes vonPlatformDescriber
. SomitPlatformDescriber
verstößt die Klasse gegen OCP.Ein anderes Beispiel:
Der "Datensatz" ist die Gruppe von Kanälen, in denen ein Protokolleintrag hinzugefügt werden soll.
Logger
ist die Komponente, die für das Hinzufügen von Einträgen zu allen Kanälen verantwortlich ist. Um die Unterstützung für eine andere Art der Protokollierung hinzuzufügen, muss der Quellcode von aktualisiert werdenLogger
. SomitLogger
verstößt die Klasse gegen OCP.Beachten Sie, dass der Datensatz in beiden Beispielen nicht semantisch festgelegt ist. Es kann sich im Laufe der Zeit ändern. Eine neue Plattform kann entstehen. Möglicherweise wird ein neuer Protokollierungskanal erstellt. Sollte Ihre Komponente in diesem Fall aktualisiert werden, verstößt sie gegen OCP.
Die Grenzen ausreizen
Nun der knifflige Teil. Vergleichen Sie die obigen Beispiele mit den folgenden:
Sie könnten denken, dass es
translateToRussian
gegen die OCP verstößt. Aber eigentlich ist es nicht.GregorianWeekDay
hat ein bestimmtes Limit von genau 7 Wochentagen mit genauen Namen. Und das Wichtigste ist, dass sich diese Grenzen semantisch nicht im Laufe der Zeit ändern können. Es wird immer 7 Tage in der Gregorianischen Woche geben. Es wird immer Montag, Dienstag usw. geben. Dieser Datensatz ist semantisch festgelegt. Es ist nicht möglich, dasstranslateToRussian
der Quellcode geändert werden muss. Somit wird OCP nicht verletzt.Jetzt sollte klar sein, dass eine anstrengende
switch
Aussage nicht immer ein Hinweis auf ein gebrochenes OCP ist.Der Unterschied
Fühle jetzt den Unterschied:
Diese Bedingungen sind vollständig orthogonal.
Beispiele
In @ Spoikes Antwort ist das Prinzip des Verstoßes gegen das eine, aber das Befolgen des anderen Teils völlig falsch.
Im ersten Beispiel
for
verletzt der -loop-Teil eindeutig OCP, da er nicht ohne Änderung erweiterbar ist. Es gibt jedoch keinen Hinweis auf eine LSP-Verletzung. Und es ist nicht einmal klar, ob derContext
Vertrag getPersons erlaubt, irgendetwas außerBoss
oder zurückzugebenPeon
. Selbst wenn ein Vertrag angenommen wird, der dieIPerson
Rückgabe einer Unterklasse ermöglicht , gibt es keine Klasse, die diese Nachbedingung außer Kraft setzt und gegen sie verstößt. Wenn getPersons eine Instanz einer dritten Klassefor
zurückgibt , erledigt die -loop ihre Arbeit ohne Fehler. Diese Tatsache hat jedoch nichts mit LSP zu tun.Nächster. Im zweiten Beispiel wird weder gegen LSP noch gegen OCP verstoßen. Auch hier hat das
Context
Teil nichts mit LSP zu tun - kein definierter Vertrag, keine Unterklassen, keine abbrechenden Überschreibungen. Es ist nichtContext
, wer LSP gehorchen sollte, esLiskovSub
sollte nicht den Vertrag seiner Basis brechen. Ist die Klasse in Bezug auf OCP wirklich geschlossen? - ja ist es. Es ist keine Änderung erforderlich, um es zu erweitern. Offensichtlich besagt der Name des Erweiterungspunkts, dass Sie alles tun können, ohne Einschränkungen . Das Beispiel ist im wirklichen Leben nicht sehr nützlich, verstößt aber eindeutig nicht gegen OCP.Lassen Sie uns versuchen, einige korrekte Beispiele mit echter Verletzung von OCP oder LSP zu machen.
Folgen Sie OCP, aber nicht LSP
Hier sind
HumanReadablePlatformSerializer
keine Änderungen erforderlich, wenn eine neue Plattform hinzugefügt wird. Somit folgt OCP.Der Vertrag sieht jedoch vor, dass
toJson
ein ordnungsgemäß formatierter JSON zurückgegeben werden muss. Die Klasse macht das nicht. Aus diesem Grund kann es nicht an eine Komponente übergeben werden, diePlatformSerializer
zum Formatieren des Hauptteils einer Netzwerkanforderung verwendet wird. DamitHumanReadablePlatformSerializer
verstößt LSP.Folgen Sie LSP, aber nicht OCP
Einige Änderungen zum vorherigen Beispiel:
Der Serializer gibt eine korrekt formatierte JSON-Zeichenfolge zurück. Also keine LSP-Verletzung hier.
Wenn die Plattform jedoch am meisten genutzt wird, muss in JSON eine entsprechende Angabe vorhanden sein. In diesem Beispiel wird OCP durch die
HumanReadablePlatformSerializer.isMostPopular
Funktion verletzt , da iOS eines Tages zur beliebtesten Plattform wird. Formal bedeutet dies, dass der Satz der am häufigsten verwendeten Plattformen derzeit als "Android" definiert ist undisMostPopular
mit diesem Datensatz nur unzureichend umgeht. Der Datensatz ist nicht semantisch festgelegt und kann sich im Laufe der Zeit frei ändern.HumanReadablePlatformSerializer
Der Quellcode von muss im Falle einer Änderung aktualisiert werden.Möglicherweise stellen Sie in diesem Beispiel auch eine Verletzung der Einzelverantwortung fest. Ich habe es absichtlich so gemacht, dass ich beide Prinzipien am selben Subjekt demonstrieren kann. Um SRP zu reparieren, können Sie die
isMostPopular
Funktion auf eine externe Ebene extrahierenHelper
und einen Parameter hinzufügenPlatformSerializer.toJson
. Aber das ist eine andere Geschichte.quelle
LSP und OCP sind nicht dasselbe.
LSP spricht über die Richtigkeit des Programms in seiner jetzigen Form . Wenn eine Instanz eines Subtyps beim Einsetzen in den Code für Vorgängertypen die Programmkorrektheit beeinträchtigen würde, liegt ein Verstoß gegen LSP vor. Möglicherweise müssen Sie einen Test nachahmen, um dies zu zeigen, aber Sie müssen die zugrunde liegende Codebasis nicht ändern. Sie validieren das Programm selbst, um festzustellen, ob es LSP erfüllt.
OCP spricht über die Richtigkeit von Änderungen im Programmcode, das Delta von einer Quellversion zur anderen. Verhalten sollte nicht geändert werden. Es sollte nur erweitert werden. Das klassische Beispiel ist die Feldaddition. Alle vorhandenen Felder arbeiten weiterhin wie bisher. Das neue Feld fügt nur Funktionalität hinzu. Das Löschen eines Feldes ist jedoch in der Regel eine Verletzung von OCP. Hier validieren Sie das Programmversions-Delta, um festzustellen, ob es OCP erfüllt.
Das ist also der Hauptunterschied zwischen LSP und OCP. Ersteres überprüft nur die aktuelle Codebasis , letzteres überprüft nur das Codebasis-Delta von einer Version zur nächsten . Als solche sind sie nicht die gleiche Sache sein können, werden sie definiert als Validierung verschiedene Dinge.
Ich gebe Ihnen einen formelleren Beweis: Zu sagen, dass "LSP impliziert OCP" ein Delta impliziert (da OCP ein anderes als das im trivialen Fall erfordert), erfordert LSP jedoch kein Delta. Das ist also eindeutig falsch. Umgekehrt können wir "OCP impliziert LSP" einfach widerlegen, indem wir sagen, OCP ist eine Aussage über Deltas, daher sagt sie nichts über eine Aussage über ein vorhandenes Programm aus. Dies ergibt sich aus der Tatsache, dass Sie ein beliebiges Delta erstellen können, indem Sie mit einem beliebigen Programm beginnen. Sie sind völlig unabhängig.
quelle
Ich würde es aus Sicht des Kunden betrachten. Wenn der Client Funktionen einer Schnittstelle verwendet und diese Funktion intern von Klasse A implementiert wurde, wird es eine Klasse B geben, die Klasse A erweitert. Wenn ich morgen Klasse A von dieser Schnittstelle entferne und Klasse B setze, sollte Klasse B dies tun Stellen Sie dem Client dieselben Funktionen zur Verfügung. Ein Standardbeispiel ist eine Entenklasse, die schwimmt, und wenn ToyDuck Duck erweitert, sollte sie auch schwimmen und sich nicht darüber beklagen, dass sie nicht schwimmen kann, da ToyDuck sonst keine erweiterte Entenklasse haben sollte.
quelle