it-swarm.com.de

LSP vs OCP / Liskov Substitution VS Open Close

Ich versuche die SOLID Prinzipien von OOP und ich bin zu dem Schluss gekommen, dass LSP und OCP einige Ähnlichkeiten haben (wenn nicht mehr zu sagen)). .

das Open/Closed-Prinzip besagt, dass "Software-Entitäten (Klassen, Module, Funktionen usw.) zur Erweiterung geöffnet, aber zur Änderung geschlossen sein sollten".

LSP in einfachen Worten besagt, dass jede Instanz von Foo durch jede Instanz von Bar ersetzt werden kann, die von Foo abgeleitet ist, und dass das Programm genauso funktioniert.

Ich bin kein Profi OOP Programmierer), aber es scheint mir, dass LSP nur möglich ist, wenn Bar, abgeleitet von Foo, nichts daran ändert Dies bedeutet, dass in einem bestimmten Programm LSP nur dann wahr ist, wenn OCP wahr ist, und OCP nur dann wahr ist, 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.

49
Kolyunya

Meine Güte, es gibt einige seltsame Missverständnisse darüber, was OCP und LSP sind, und einige sind auf die Nichtübereinstimmung einiger Terminologien und verwirrender Beispiele zurückzuführen. Beide Prinzipien sind nur dann "dasselbe", wenn Sie sie auf dieselbe Weise implementieren. Muster folgen normalerweise mit wenigen Ausnahmen auf die eine oder andere Weise den Prinzipien.

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 verlängern in diesem Fall nicht unbedingt bedeutet, dass Sie die tatsächliche Klasse unterordnen sollten, die das neue Verhalten benötigt. Sehen Sie, wie ich bei der ersten Nichtübereinstimmung der Terminologie erwähnt habe? Das Schlüsselwort extend bedeutet nur Unterklasse in Java, aber die Prinzipien sind älter als Java.

Das Original stammt von Bertrand Meyer im Jahr 1988:

Software-Entitäten (Klassen, Module, Funktionen usw.) sollten zur Erweiterung geöffnet, aber zur Änderung geschlossen sein.

Hier ist es viel klarer, dass das Prinzip auf Software-Entitäten angewendet wird. Ein schlechtes Beispiel wäre das Überschreiben der Software-Entität, 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 hierfür ist die Implementierung des Strategie-Musters (da es meiner Meinung nach am einfachsten ist, das GoF-Muster-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 ist Context gesperrt für weitere Änderungen. Die meisten Programmierer würden wahrscheinlich die Klasse in Unterklassen unterteilen wollen, um sie zu erweitern, aber hier nicht, weil davon ausgegangen wird, dass ihr Verhalten durch alles, was die Schnittstelle IBehavior implementiert, geändert kann.

Das heißt, Die Kontextklasse ist zur Änderung geschlossen, aber zur Erweiterung geöffnet. Es folgt tatsächlich einem anderen Grundprinzip, weil wir das Verhalten mit Objektzusammensetzung anstelle von Vererbung setzen:

"Bevorzugen Sie ' Objektzusammensetzung ' gegenüber ' Klassenvererbung '." (Gang of Four 1995: 20)

Ich werde den Leser über dieses Prinzip nachlesen lassen, 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!");
    }
}

Mit diesem Muster können wir das Verhalten des Kontexts zur Laufzeit über die Methode setBehavior 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, tun Sie dies, indem Sie die "offene" kollaborierende Abhängigkeit in Unterklassen unterteilen. Dies ist eindeutig nicht dasselbe wie das Unterklassifizieren des Kontexts selbst, es ist jedoch OCP. LSP erwähnt dies auch 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 Erweiterungen offen zu halten. Dies ist nützlich, z.B. in Sprachen, die eher prototypbasiert als klassenbasiert sind. Die Idee ist, ein dynamisches Objekt nach Bedarf mit mehr Methoden oder Attributen zu ändern, dh Objekte, die mit anderen Objekten gemischt oder "gemischt" 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 Idee ist, die Objekte dynamisch zu erweitern, und der Vorteil davon ist, dass Objekte Methoden gemeinsam nutzen können, selbst wenn sie sich in völlig unterschiedlichen Domänen befinden. Im obigen Fall können Sie problemlos andere Arten von HTML-Ankern erstellen, indem Sie Ihre spezifische Implementierung mit LinkMixin erweitern.

In Bezug auf OCP sind die "Mixins" Erweiterungen. Im obigen Beispiel ist YoutubeLink unsere Software-Entität, die zur Änderung 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 wirklich schlechte Sache, und ich werde weiter unten erklären, dass das Überprüfen auf Typen im Allgemeinen eine schlechte Idee ist und die Idee mit Polymorphismus bricht.

Beachten Sie, dass mit dieser Methode eine Mehrfachvererbung möglich ist, da die meisten extend - Implementierungen mehrere Objekte einmischen können:

_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);

Das einzige, was Sie beachten müssen, ist, die Namen nicht zu kollidieren, d. H. 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 passiert, ist dies ein Hinweis auf ein fehlerhaftes Design.

Liskovs Substitutionsprinzip (LSP)

Onkel Bob definiert es einfach durch:

Abgeleitete Klassen müssen ihre Basisklassen ersetzen können.

Dieses Prinzip ist alt, tatsächlich unterscheidet die Definition von Onkel Bob die Prinzipien nicht, da LSP dadurch 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 ungefähr die folgende Substitutionseigenschaft: Wenn für jedes Objekt o1 vom Typ S gibt es ein Objekt o2 vom Typ T, so dass für alle Programme P, die in Bezug auf T definiert sind, das Verhalten von P unverändert bleibt, wenn o1 ersetzt o2 dann ist S ein Subtyp von T.

Lassen Sie uns eine Weile mit den Schultern zucken, beachten Sie, dass Klassen überhaupt nicht erwähnt werden. In JavaScript können Sie LSP tatsächlich folgen, obwohl es nicht explizit klassenbasiert ist. Wenn Ihr Programm eine Liste mit mindestens einigen JavaScript-Objekten enthält, die:

  • muss auf die gleiche Weise berechnet werden,
  • das gleiche Verhalten haben, und
  • sind sonst irgendwie völlig anders

... dann werden die Objekte als vom gleichen "Typ" angesehen und es ist für das Programm nicht wirklich wichtig. Dies ist im Wesentlichen Polymorphismus . Im allgemeinen Sinne; Sie sollten den tatsächlichen Subtyp nicht kennen müssen, wenn Sie die Schnittstelle verwenden. OCP sagt dazu nichts explizites. Es zeigt auch tatsächlich einen Designfehler auf, den die meisten Programmieranfänger machen:

Wenn Sie den Drang verspüren, den Subtyp eines Objekts zu überprüfen, tun Sie dies höchstwahrscheinlich FALSCH.

Okay, es ist vielleicht nicht immer falsch, aber wenn Sie den Drang haben, Typprüfung mit instanceof oder Aufzählungen durchzuführen, machen Sie das Programm möglicherweise etwas mehr für sich selbst verwickelt, als es sein muss. Dies ist jedoch nicht immer der Fall; Schnelle und schmutzige Hacks, um die Dinge zum Laufen zu bringen, sind für mich eine gute Konzession, wenn die Lösung klein genug ist und wenn Sie gnadenloses Refactoring üben, kann sie verbessert werden, sobald Änderungen dies erfordern.

Abhängig vom eigentlichen Problem gibt es Möglichkeiten, diesen "Konstruktionsfehler" zu umgehen:

  • Die Superklasse ruft die Voraussetzungen nicht auf und zwingt den Aufrufer stattdessen dazu.
  • Der Superklasse fehlt eine generische Methode, die der Aufrufer benötigt.

Beides sind häufige "Fehler" beim Code-Design. Es gibt verschiedene Refactorings, die Sie durchführen können, z. B. Pull-up-Methode oder Refactor für ein Muster wie Besuchermuster .

Ich mag das Besuchermuster sehr, da es große Spaghetti mit if-Anweisungen verarbeiten kann und einfacher zu implementieren ist als das, was Sie über vorhandenen Code denken 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 eine boolesche canDo -Methode haben, kann der Implementierer diesen Methodenaufruf verwenden, um festzustellen, ob es das richtige Objekt für die Ausführung des Jobs ist. 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, aber beide zeigen unterschiedliche Dinge über sie auf. 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 diese im Produktionscode auftauchen (und noch schlimmer):

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 zur Änderung geschlossen, aber zur Erweiterung geöffnet ist. Die Dinge nehmen jedoch eine dunkle Wendung, wenn wir sie verwenden 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 die Typprüfung und die Typkonvertierung durchführen! Erinnern Sie sich, wie ich oben erwähnt habe, wie Typprüfung eine schlechte Sache ist? Ach nein! Aber keine Angst, wie auch oben erwähnt, entweder ein Pull-up-Refactoring durchführen oder ein Besuchermuster implementieren. 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 jetzt, dass Sie den genauen Typ nach LSP nicht mehr 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 irgendwie erfunden, aber bei diesem 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, dass dieser Code auch OCP folgt, aber schauen Sie genau hin, ist die Klasse wirklich geschlossen? Was wäre, wenn die Methode doStuff mehr als nur eine Zeile ausdrucken würde?

Die Antwort, wenn es OCP folgt, lautet einfach: [~ # ~] nein [~ # ~] , es liegt nicht daran, dass wir in diesem Objektdesign ' Es ist erforderlich, den Code vollständig mit etwas anderem zu überschreiben. Dies öffnet das Ausschneiden und Einfügen von Würmern, da Sie Code aus der Basisklasse kopieren müssen, damit alles funktioniert. Die Methode doStuff kann zwar erweitert werden, wurde jedoch nicht vollständig für Änderungen geschlossen.

Wir können das Template-Methodenmuster darauf anwenden. Das Muster der Vorlagenmethode ist in Frameworks so häufig, dass Sie es möglicherweise verwendet haben, ohne es zu kennen (z. B. Java Swing-Komponenten, C # -Formulare und -Komponenten usw.). Hier ist diese eine Möglichkeit zum Schließen Die Methode doStuff zum Ändern und Sicherstellen, dass sie geschlossen bleibt, indem sie mit dem Schlüsselwort final von Java markiert wird. Dieses Schlüsselwort verhindert, dass jemand die Klasse weiter unterordnet (in C # können Sie sealed um dasselbe zu 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 zu sein, was es ist, aber stellen Sie sich vor, dies würde mit mehr Code vergrößert. Ich sehe immer wieder Code, der in der Produktion bereitgestellt wird, wo Unterklassen alles vollständig überschreiben und der überschriebene Code meistens zwischen Implementierungen ausgeschnitten und eingefügt wird. Es funktioniert, aber wie bei allen Codeduplizierungen ist es auch eine Einrichtung für Wartungs-Albträume.

Fazit

Ich hoffe, dies alles klärt einige Fragen zu OCP und LSP und den Unterschieden/Ähnlichkeiten zwischen ihnen. Es ist leicht, sie als gleich abzulehnen, aber die obigen Beispiele sollten zeigen, dass dies nicht der Fall ist.

Beachten Sie Folgendes: Sammeln Sie den obigen Beispielcode:

  • Bei OCP geht es darum, den Arbeitscode zu sperren, ihn aber dennoch mit einigen Erweiterungspunkten offen zu halten.

    Auf diese Weise wird eine Codeduplizierung vermieden, indem der Code gekapselt wird, der sich wie im Beispiel des Musters für die Vorlagenmethode ändert. Es ermöglicht auch ein schnelles Versagen, da das Brechen von Änderungen schmerzhaft ist (d. H. Eine Stelle wechseln, überall anders brechen). Aus Gründen der Wartung ist das Konzept der Verkapselung von Änderungen eine gute Sache, da Änderungen immer auftreten.

  • Bei LSP geht es darum, den Benutzer verschiedene Objekte behandeln zu lassen, die einen Supertyp implementieren, ohne zu überprüfen, um welchen tatsächlichen Typ es sich handelt. Darum geht es in Polymorphismus.

    Dieses Prinzip bietet eine Alternative zur Typprüfung und Typkonvertierung, die mit zunehmender Anzahl von Typen außer Kontrolle geraten kann und durch Pull-up-Refactoring oder Anwenden von Mustern wie Visitor erreicht werden kann.

118
Spoike

Dies ist etwas, das viel Verwirrung stiftet. Ich ziehe es vor, diese Prinzipien etwas philosophisch zu betrachten, weil es viele verschiedene Beispiele für sie gibt und manchmal konkrete Beispiele nicht wirklich ihre gesamte Essenz erfassen.

Was OCP zu beheben versucht

Angenommen, wir müssen einem bestimmten Programm Funktionen hinzufügen. Der einfachste Weg, dies zu tun, insbesondere für Personen, die darin geschult wurden, prozedural zu denken, besteht darin, eine if-Klausel hinzuzufügen, wo immer dies erforderlich ist, oder ähnliches.

Die Probleme damit sind

  1. Es ändert den Fluss des vorhandenen Arbeitscodes.
  2. Es erzwingt in jedem Fall eine neue bedingte Verzweigung. Angenommen, Sie haben eine Liste mit Büchern, von denen einige zum Verkauf stehen, und Sie möchten alle Bücher durchlaufen und ihren Preis ausdrucken. Wenn sie zum Verkauf angeboten werden, enthält der gedruckte Preis die Zeichenfolge " (IM ANGEBOT)".

Sie können dies tun, indem Sie allen Büchern mit dem Namen "is_on_sale" ein zusätzliches Feld hinzufügen. Anschließend können Sie dieses Feld beim Drucken eines Buchpreises oder alternativ überprüfen. Sie können Verkaufsbücher aus der Datenbank mit einem anderen Typ instanziieren, der "(ON SALE)" in der Preiszeichenfolge druckt (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 es viele Fälle geben kann, in denen unterschiedliche Daten und Logik erforderlich sind, und Sie werden sehen, warum es eine gute Idee ist, OCP beim Entwerfen Ihrer Klassen zu berücksichtigen oder auf Änderungen der Anforderungen zu reagieren.

Inzwischen sollten Sie die Hauptidee haben: Versuchen Sie, sich in eine Situation zu versetzen, in der neuer Code als polymorphe Erweiterungen implementiert werden kann, nicht als prozedurale Änderungen.

Aber haben Sie niemals Angst, den Kontext zu analysieren und festzustellen, ob die Nachteile die Vorteile überwiegen, denn selbst ein Prinzip wie OCP kann aus einem 20-Zeilen-Programm ein Durcheinander von 20 Klassen machen, wenn es nicht sorgfältig behandelt wird.

Was LSP zu beheben versucht

Wir alle lieben die Wiederverwendung von Code. Eine Folge davon ist, dass viele Programme es nicht vollständig verstehen, bis zu dem Punkt, an dem sie gemeinsame Codezeilen blind faktorisieren, nur um unlesbare Komplexitäten und redundante enge Kopplungen zwischen Modulen zu erzeugen, die außer einigen Codezeilen Ich habe nichts gemeinsam, was die konzeptionelle Arbeit betrifft.

Das größte Beispiel hierfür ist Wiederverwendung der Schnittstelle. Sie haben es wahrscheinlich selbst gesehen; Eine Klasse implementiert eine Schnittstelle, nicht weil es sich um eine logische Implementierung handelt (oder um eine Erweiterung bei konkreten Basisklassen), sondern weil die Methoden, die sie zu diesem Zeitpunkt deklariert, die richtigen Signaturen haben.

Aber dann stoßen Sie 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 Orte übergeben, die völlig andere Funktionen erfordern, die nur von ähnlichen Signaturen abhängen.

Das ist nicht so schrecklich, aber es verursacht viel Verwirrung und wir haben die Technologie, um zu verhindern, dass wir solche Fehler machen. Was wir tun müssen, ist, Schnittstellen als API + Protokoll zu behandeln. Die API ist in Deklarationen ersichtlich, und das Protokoll ist in bestehenden Verwendungen der Schnittstelle ersichtlich. Wenn wir zwei konzeptionelle Protokolle haben, die dieselbe API verwenden, sollten sie als zwei verschiedene Schnittstellen dargestellt werden. Andernfalls geraten wir in DRY Dogmatismus) und schaffen ironischerweise nur schwer zu pflegenden Code.

Jetzt sollten Sie die Definition perfekt verstehen können. LSP sagt: Erben Sie nicht von einer Basisklasse und implementieren Sie Funktionen in den Unterklassen, mit denen andere Orte, die von der Basisklasse abhängen, nicht auskommen.

15
Yam Marcovic

Meinem Verständnis nach:

OCP sagt: "Wenn Sie eine neue Funktion hinzufügen, 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 sie vollständig mit ihrer Basis austauschbar ist."

Ich denke, sie ergänzen sich, aber sie sind nicht gleich.

8
henginy

Während es stimmt, dass OCP und LSP beide mit Modifikation zu tun haben, ist die Art der Modifikation, über die OCP spricht, nicht die, über die LSP spricht.

Das Ändern in Bezug auf OCP ist die physische Aktion eines Entwicklers Schreiben von Code in einer vorhandenen Klasse.

LSP befasst sich mit der Verhaltensänderung, die eine abgeleitete Klasse im Vergleich zu ihrer Basisklasse mit sich bringt, und der Laufzeit Änderung der Programmausführung, die durch die Verwendung der Unterklasse anstelle der Oberklasse verursacht werden kann.

Also, obwohl sie aus der Ferne ähnlich aussehen könnten OCP! = LSP. Tatsächlich denke ich, dass sie die einzigen 2 SOLID Prinzipien sind, die nicht in Bezug aufeinander verstanden werden können).

4
guillaume31

LSP in einfachen Worten besagt, dass jede Instanz von Foo durch jede Instanz von Bar ersetzt werden kann, die von Foo abgeleitet ist, ohne dass die Programmfunktionalität verloren geht.

Das ist falsch. LSP gibt an, dass die Klasse Bar kein Verhalten einführen sollte, das nicht erwartet wird, wenn Code Foo verwendet, wenn Bar von Foo abgeleitet ist. Es hat nichts mit Funktionsverlust zu tun. Sie können Funktionen entfernen, jedoch nur, wenn Code mit Foo nicht von dieser Funktionalität abhängt.

Letztendlich ist dies jedoch normalerweise schwer zu erreichen, da der Code, der Foo verwendet, meistens von seinem gesamten Verhalten abhängt. Das Entfernen verletzt also LSP. Eine solche Vereinfachung ist jedoch nur ein Teil von LSP.

2
Euphoric

LSP und OCP sind nicht dasselbe.

LSP spricht über die Richtigkeit des Programms wie es ist. Wenn eine Instanz eines Subtyps die Programmkorrektheit beeinträchtigen würde, wenn sie in den Code für Vorfahrentypen eingesetzt wird, haben Sie eine Verletzung von LSP nachgewiesen. 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 entspricht.

OCP spricht über die Richtigkeit von Änderungen im Programmcode, dem Delta von einer Quellversion zur anderen. Das Verhalten sollte nicht geändert werden. Es sollte nur verlängert werden. Das klassische Beispiel ist die Feldaddition. Alle vorhandenen Felder funktionieren weiterhin wie bisher. Das neue Feld fügt lediglich Funktionen hinzu. Das Löschen eines Feldes ist jedoch normalerweise eine Verletzung von OCP. Hier validieren Sie das Programmversion Delta, um festzustellen, ob es OCP erfüllt.

Das ist also der Hauptunterschied zwischen LSP und OCP. Ersteres validiert nur das Codebasis wie sie ist, letzteres validiert nur Codebasisdelta von einer Version zur nächsten. Als solche können sie nicht dasselbe sein, sie sind definiert als Validierung verschiedener Dinge.

Ich gebe Ihnen einen formelleren Beweis: Zu sagen, "LSP impliziert OCP" würde ein Delta implizieren (weil OCP ein anderes als im trivialen Fall erfordert), aber LSP benötigt keines. Das ist also eindeutig falsch. Umgekehrt können wir "OCP impliziert LSP" einfach widerlegen, indem wir sagen, dass OCP eine Aussage über Deltas ist, daher sagt es nichts über eine Aussage über ein vorhandenes Programm aus. Dies folgt aus der Tatsache, dass Sie JEDES Delta erstellen können, beginnend mit JEDEM vorhandenen Programm. Sie sind völlig unabhängig.

0
Brad Thomas

Über Objekte, die möglicherweise verletzen

Um den Unterschied zu verstehen, sollten Sie Themen beider Prinzipien verstehen. Es ist kein abstrakter Teil des Codes oder der Situation, der gegen ein Prinzip verstößt oder nicht. Es ist immer eine bestimmte Komponente - Funktion, Klasse oder ein Modul -, die OCP oder LSP verletzen kann.

Wer kann LSP verletzen

Man kann nur prüfen, ob LSP defekt ist, wenn es eine Schnittstelle mit einem Vertrag und eine Implementierung dieser Schnittstelle gibt. Wenn die Implementierung nicht der Schnittstelle oder im Allgemeinen dem Vertrag entspricht, ist der LSP fehlerhaft.

Einfachstes Beispiel:

class Container {
    // Should add the object to the container.
    void addObject(object) {
        internalArray.append(object);
    }

    int size() {
        return internalArray.size();
    }
}

class CustomContainer extends Container {
    @Override void addObject(object) {
        System.console.print("Skipping object! Ha-ha!");
    }
}

void fillWithRandomNumbers(Container container) {
    while (container.size() < 42) {
        container.addObject(Randomizer.getNumber())
    }
}

Der Vertrag sieht eindeutig vor, dass addObject sein Argument an den Container anhängen soll. Und CustomContainer bricht diesen Vertrag eindeutig. Somit verletzt die Funktion CustomContainer.addObject LSP. Somit verletzt die Klasse CustomContainer LSP. Die wichtigste Konsequenz ist, dass CustomContainer nicht an fillWithRandomNumbers() übergeben werden kann. Container kann nicht durch CustomContainer ersetzt werden.

Beachten Sie einen sehr wichtigen Punkt. Es ist nicht dieser ganze Code, der LSP bricht, sondern speziell CustomContainer.addObject Und im Allgemeinen CustomContainer, der LSP bricht. Wenn Sie angeben, dass LSP verletzt wird, sollten Sie immer zwei Dinge angeben:

  • Die Entität, die gegen LSP verstößt.
  • Der Vertrag, der vom Unternehmen gebrochen wird.

Das ist es. Nur ein Vertrag und seine 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 können und der Quellcode der Komponente geändert werden muss, verstößt die Komponente gegen OCP.

Klingt komplex. Versuchen wir ein einfaches Beispiel:

enum Platform {
    iOS,
    Android
}

class PlatformDescriber {
    String describe(Platform platform) {
        switch (platform) {
            case iOS: return "iPhone OS, v10.0.1";
            case Android: return "Android, v7.1";
        }
    }
}

Der Datensatz ist der Satz unterstützter Plattformen. PlatformDescriber ist die Komponente, die Werte aus diesem Datensatz verarbeitet. Das Hinzufügen einer neuen Plattform erfordert das Aktualisieren des Quellcodes von PlatformDescriber. Somit verletzt die Klasse PlatformDescriber OCP.

Ein anderes Beispiel:

class Shop {
    void sellItemToCustomer(item, customer) {
        // some buisiness logic here
        ...
        logger.logItemSold()
    }
}

class Logger {
    void logItemSold() {
        logger.logToStdErr("an item was sold")
        logger.logToRemote("an item was sold")
        logger.logToDatabase("an item was sold")
    }
}

Der "Datensatz" ist der Satz 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 Unterstützung für eine andere Art der Protokollierung hinzuzufügen, muss der Quellcode von Logger aktualisiert werden. Somit verletzt die Klasse Logger OCP.

Beachten Sie, dass in beiden Beispielen der Datensatz nicht semantisch festgelegt ist. Es kann sich im Laufe der Zeit ändern. Eine neue Plattform kann entstehen. Möglicherweise entsteht ein neuer Protokollierungskanal. Wenn Ihre Komponente in diesem Fall aktualisiert werden sollte, verstößt sie gegen OCP.

Die Grenzen ausreizen

Nun der schwierige Teil. Vergleichen Sie die obigen Beispiele mit den folgenden:

enum GregorianWeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

String translateToRussian(GregorianWeekDay weekDay) {
    switch (weekDay) {
        case Monday: return "Понедельник";
        case Tuesday: return "Вторник";
        case Wednesday: return "Среда";
        case Thursday: return "Четверг";
        case Friday: return "Пятница";
        case Saturday: return "Суббота";
        case Sunday: return "Воскресенье";
    }
}

Sie könnten denken, dass translateToRussian OCP verletzt. Aber eigentlich ist es nicht. GregorianWeekDay hat ein spezifisches Limit von genau 7 Wochentagen mit genauen Namen. Und das Wichtigste ist, dass sich diese Grenzen im Laufe der Zeit semantisch nicht ändern können. In der gregorianischen Woche gibt es immer 7 Tage. Es wird immer Montag, Dienstag usw. geben. Dieser Datensatz ist semantisch festgelegt. Es ist nicht möglich, dass der Quellcode von translateToRussian geändert werden muss. Somit wird OCP nicht verletzt.

Jetzt sollte klar sein, dass eine anstrengende switch -Anweisung nicht immer ein Hinweis auf eine fehlerhafte OCP ist.

Der Unterschied

Fühle jetzt den Unterschied:

  • Das Thema von LSP ist "eine Implementierung von Schnittstelle/Vertrag". Wenn die Implementierung nicht dem Vertrag entspricht, wird der LSP unterbrochen. Es ist nicht wichtig, ob sich diese Implementierung im Laufe der Zeit ändert oder nicht, ob sie erweiterbar ist oder nicht.
  • Das Thema OCP ist "eine Möglichkeit, auf eine Änderung der Anforderungen zu reagieren". Wenn für die Unterstützung eines neuen Datentyps der Quellcode der Komponente geändert werden muss, die diese Daten verarbeitet, bricht diese Komponente OCP. Es ist nicht wichtig, ob die Komponente ihren Vertrag bricht oder nicht.

Diese Bedingungen sind vollständig orthogonal.

Beispiele

In @ Spoikes Antwort ist die Verletzung eines Prinzips, aber die Befolgung des anderen Teils völlig falsch.

Im ersten Beispiel verstößt der for- Schleifenteil eindeutig gegen OCP, da er ohne Änderung nicht erweiterbar ist. Es gibt jedoch keinen Hinweis auf eine LSP-Verletzung. Und es ist nicht einmal klar, ob der Vertrag Context es getPersons erlaubt, etwas anderes als Boss oder Peon zurückzugeben. Selbst wenn ein Vertrag angenommen wird, der die Rückgabe einer Unterklasse IPerson ermöglicht, gibt es keine Klasse, die diese Nachbedingung überschreibt und gegen sie verstößt. Wenn getPersons eine Instanz einer dritten Klasse zurückgibt, erledigt die for- Schleife ihre Aufgabe ohne Fehler. Diese Tatsache hat jedoch nichts mit LSP zu tun.

Nächster. Im zweiten Beispiel wird weder LSP noch OCP verletzt. Auch hier hat der Teil Context nichts mit LSP zu tun - kein definierter Vertrag, keine Unterklassen, keine Überschreibungen. Es ist nicht Context, der LSP gehorchen soll, es ist LiskovSub, der den Vertrag seiner Basis nicht brechen sollte. In Bezug auf OCP ist die Klasse wirklich geschlossen? - ja, das ist es. Es ist keine Änderung erforderlich, um es zu erweitern. Offensichtlich lautet der Name des Erweiterungspunkts . Mach alles, was du willst, keine Grenzen . Das Beispiel ist im wirklichen Leben nicht sehr nützlich, verstößt aber eindeutig nicht gegen OCP.

Versuchen wir, einige korrekte Beispiele mit einer echten Verletzung von OCP oder LSP zu erstellen.

Folgen Sie OCP, aber nicht LSP

interface Platform {
    String name();
    String version();
}

class iOS implements Platform {
    @Override String name() { return "iOS"; }
    @Override String version() { return "10.0.1"; }
}

interface PlatformSerializer {
    String toJson(Platform platform);
}

class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return platform.name() + ", v" + platform.version();
    }
}

Hier erfordert HumanReadablePlatformSerializer keine Änderungen, wenn eine neue Plattform hinzugefügt wird. Somit folgt OCP.

Der Vertrag verlangt jedoch, dass toJson einen ordnungsgemäß formatierten JSON zurückgibt. Die Klasse macht das nicht. Aus diesem Grund kann es nicht an eine Komponente übergeben werden, die PlatformSerializer zum Formatieren des Hauptteils einer Netzwerkanforderung verwendet. Somit verletzt HumanReadablePlatformSerializer LSP.

Folgen Sie LSP, aber nicht OCP

Einige Änderungen am vorherigen Beispiel:

class Android implements Platform {
    @Override String name() { return "Android"; }
    @Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return "{ "
                + "\"name\": \"" + platform.name() + "\","
                + "\"version\": \"" + platform.version() + "\","
                + "\"most-popular\": " + isMostPopular(platform) + ","
                + "}"
    }

    boolean isMostPopular(Platform platform) {
        return (platform instanceof Android)
    }
}

Der Serializer gibt eine korrekt formatierte JSON-Zeichenfolge zurück. Also hier keine LSP-Verletzung.

Es besteht jedoch die Anforderung, dass in JSON eine entsprechende Angabe vorhanden sein muss, wenn die Plattform am häufigsten verwendet wird. In diesem Beispiel wird OCP durch die Funktion HumanReadablePlatformSerializer.isMostPopular 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 und isMostPopular diesen Datensatz nicht ausreichend verarbeitet. Der Datensatz ist nicht semantisch festgelegt und kann sich im Laufe der Zeit frei ändern. Der Quellcode von HumanReadablePlatformSerializer muss im Falle einer Änderung aktualisiert werden.

In diesem Beispiel stellen Sie möglicherweise auch einen Verstoß gegen die Einzelverantwortung fest. Ich habe dies absichtlich gemacht, um beide Prinzipien in derselben Facheinheit demonstrieren zu können. Um SRP zu reparieren, können Sie die Funktion isMostPopular in ein externes Helper extrahieren und PlatformSerializer.toJson Einen Parameter hinzufügen. Aber das ist eine andere Geschichte.

0
mekarthedev