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 :
Sie sollten in der Lage sein, das Verhalten einer Klasse zu erweitern, ohne es zu ändern.
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:
Software-Entitäten (Klassen, Module, Funktionen usw.) sollten zur Erweiterung geöffnet, zur Änderung jedoch geschlossen sein.
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):
// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {
// Context is however open for extension through
// this private field
private IBehavior behavior;
// The context calls the behavior in this public
// method. If you want to change this you need
// to implement it in the IBehavior object
public void doStuff() {
if (this.behavior != null)
this.behavior.doStuff();
}
// You can dynamically set a new behavior at will
public void setBehavior(IBehavior behavior) {
this.behavior = behavior;
}
}
// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
public void doStuff();
}
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 kann IBehavior
.
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:
"Bevorzugen Sie die ' Objektzusammensetzung ' gegenüber der ' Klassenvererbung '." (Viererbande 1995: 20)
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:
public class HelloWorldBehavior implements IBehavior {
public void doStuff() {
System.println("Hello world!");
}
}
public class GoodByeBehavior implements IBehavior {
public void doStuff() {
System.out.println("Good bye cruel world!");
}
}
Mithilfe dieses Musters können wir das Verhalten des Kontexts zur Laufzeit über die setBehavior
Methode als Erweiterungspunkt ändern .
// in your main method
Context c = new Context();
c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"
c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"
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:
// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
render: function() {
return '<a href="' + this.link +'">'
+ this.content
+ '</a>;
}
}
// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
this.content = content;
this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
setLink: function(youtubeid) {
this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
}
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);
// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");
console.log(ytLink.render());
// will output:
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>
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:
_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);
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:
Abgeleitete Klassen müssen durch ihre Basisklassen ersetzt werden können.
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:
Was hier gewünscht wird, ist etwa die folgende Substitutionseigenschaft: Wenn für jedes Objekt o1
vom Typ S
ein Objekt o2
vom Typ vorhanden ist, T
so dass für alle P
in Bezug auf definierten Programme T
das Verhalten von P
unverändert bleibt, wenn o1
für ersetzt wird, o2
dann S
ist es ein Subtyp von T
.
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:
- muss auf die gleiche Weise berechnet werden,
- dasselbe Verhalten haben und
- sind sonst irgendwie ganz anders
... 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:
- Die Superklasse ruft nicht die Voraussetzungen auf, sondern zwingt den Aufrufer dazu.
- Der Superklasse fehlt eine generische Methode, die der Aufrufer benötigt.
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:
public class Context {
public void doStuff(string query) {
// outcome no. 1
if (query.Equals("Hello")) {
System.out.println("Hello world!");
}
// outcome no. 2
else if (query.Equals("Bye")) {
System.out.println("Good bye cruel world!");
}
// a change request may require another outcome...
}
}
// usage:
Context c = new Context();
c.doStuff("Hello");
// prints "Hello world"
c.doStuff("Bye");
// prints "Bye"
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:
public interface IVisitor {
public bool canDo(string query);
public void doStuff();
}
// outcome 1
public class HelloVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Hello");
}
public void doStuff() {
System.out.println("Hello World");
}
}
// outcome 2
public class ByeVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Bye");
}
public void doStuff() {
System.out.println("Good bye cruel world");
}
}
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):
public class Context {
private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();
public Context() {
visitors.add(new HelloVisitor());
visitors.add(new ByeVisitor());
}
// instead of if-statements, go through all visitors
// and use the canDo method to determine if the
// visitor object is the right one to "visit"
public void doStuff(string query) {
for(IVisitor visitor : visitors) {
if (visitor.canDo(query)) {
visitor.doStuff();
break;
// or return... it depends if you have logic
// after this foreach loop
}
}
}
// dynamically adds new visitors
public void addVisitor(IVisitor visitor) {
if (visitor != null)
visitors.add(visitor);
}
}
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:
public interface IPerson {}
public class Boss implements IPerson {
public void doBossStuff() { ... }
}
public class Peon implements IPerson {
public void doPeonStuff() { ... }
}
public class Context {
public Collection<IPerson> getPersons() { ... }
}
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:
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// now we have to check the type... :-P
if (person instanceof Boss) {
((Boss) person).doBossStuff();
}
else if (person instanceof Peon) {
((Peon) person).doPeonStuff();
}
}
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:
public class Boss implements IPerson {
// we're adding this general method
public void doStuff() {
// that does the call instead
this.doBossStuff();
}
public void doBossStuff() { ... }
}
public interface IPerson {
// pulled up method from Boss
public void doStuff();
}
// do the same for Peon
Der Vorteil ist nun, dass Sie nach LSP nicht mehr den genauen Typ kennen müssen:
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// yay, no type checking!
person.doStuff();
}
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:
public class LiskovBase {
public void doStuff() {
System.out.println("My name is Liskov");
}
}
public class LiskovSub extends LiskovBase {
public void doStuff() {
System.out.println("I'm a sub Liskov!");
}
}
public class Context {
private LiskovBase base;
// the good stuff
public void doLiskovyStuff() {
base.doStuff();
}
public void setBase(LiskovBase base) { this.base = base }
}
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 dem final
Schlüsselwort von Java markieren . Dieses Schlüsselwort verhindert, dass die Klasse weiter in Unterklassen unterteilt wird (in C # können Sie sealed
dasselbe tun).
public class LiskovBase {
// this is now a template method
// the code that was duplicated
public final void doStuff() {
System.out.println(getStuffString());
}
// extension point, the code that "varies"
// in LiskovBase and it's subclasses
// called by the template method above
// we expect it to be virtual and overridden
public string getStuffString() {
return "My name is Liskov";
}
}
public class LiskovSub extends LiskovBase {
// the extension overridden
// the actual code that varied
public string getStuffString() {
return "I'm sub Liskov!";
}
}
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.