Frage

Ich habe gehört, dass das Liskov Substitution Principle (LSP) ist ein Grundprinzip der objektorientierten design.Was ist es und was sind einige Beispiele für seine Verwendung?

War es hilfreich?

Lösung 2

Das Liskov Substitutionsprinzip (LSP, ) ist ein Konzept in der objektorientierten Programmierung, die besagt:

  

Funktionen, die Zeiger verwenden oder   Verweise auf Basisklassen müssen   der Lage, Objekte von abgeleiteten Klassen zu verwenden   ohne es zu wissen.

In seinem Herzen LSP ist über Schnittstellen und Verträge sowie wie zu entscheiden, wann eine Klasse gegen die Verwendung eine andere Strategie, wie Zusammensetzung zu verlängern Ihr Ziel zu erreichen.

Der effektivste Weg, den ich gesehen habe, diesen Punkt zu veranschaulichen, wurde in Head First OOA & D . Sie präsentieren ein Szenario, in dem Sie einen Entwickler an einem Projekt zur Schaffung eines Rahmens für Strategie-Spiele zu bauen.

Sie stellen eine Klasse, die ein Brett darstellt, der wie folgt aussieht:

Klassendiagramm

Alle Methoden nehmen X und Y-Koordinaten als Parameter die Fliese Position in der zweidimensionalen Anordnung von Tiles zu lokalisieren. Dies wird ein Spieleentwickler erlaubt Einheiten in dem Vorstand im Laufe des Spiels zu verwalten.

Das Buch geht auf die Anforderungen zu ändern zu sagen, dass das Spiel Rahmen der Arbeit auch 3D-Spielbretter unterstützt, muß Spiele aufzunehmen, den Flug. So eine ThreeDBoard Klasse wird eingeführt, das Board erstreckt.

Auf den ersten Blick scheint dies eine gute Entscheidung. Board bietet sowohl die Height und Width Eigenschaften und ThreeDBoard stellt die Z-Achse.

Wenn es bricht, wenn Sie alle anderen Mitglieder sehen von Board geerbt. Die Methoden für AddUnit, GetTile, GetUnits und so weiter, alle nehmen beide X- und Y-Parameter in der Board Klasse, aber die ThreeDBoard braucht einen Z-Parameter als auch.

So können Sie diese Methoden wieder mit einem Z-Parameter implementieren müssen. Die Z-Parameter haben keinen Zusammenhang mit der Board Klasse und die geerbten Methoden aus der Board Klasse verlieren ihre Bedeutung. Eine Einheit des Code versucht, die ThreeDBoard Klasse als Basisklasse Board zu verwenden wäre sehr von Glück.

Vielleicht sollten wir einen anderen Ansatz finden. Statt Board auszudehnen, ThreeDBoard sollte Board Objekten zusammengesetzt werden. Ein Board Objekt pro Einheit der Z-Achse.

Dies ermöglicht es uns, gute objektorientierte Prinzipien wie Kapselung und Wiederverwendung zu verwenden und LSP nicht verletzen.

Andere Tipps

Ein gutes Beispiel veranschaulicht, LSP (gegeben von Onkel Bob in einem Podcast Ich habe vor kurzem gehört) war, wie manchmal etwas, das direkt in der natürlichen Sprache klingt funktioniert nicht ganz im Code.

In der Mathematik ist ein Square ein Rectangle. Tatsächlich ist es eine Spezialisierung eines Rechtecks. Die „ist ein“ machen Sie dies wollen mit Vererbung modellieren. Wenn jedoch in Code, den Sie Square von Rectangle ableiten gemacht, dann sollte ein Square verwendbar sein überall Sie Rectangle erwarten. Dies sorgt für ein seltsames Verhalten.

Stellen Sie sich SetWidth und SetHeight Methoden auf Ihrer Rectangle Basisklasse hatte; dies scheint durchaus logisch. Allerdings, wenn Ihr Rectangle Verweis auf eine Square spitz, dann SetWidth und SetHeight macht keinen Sinn, weil eine Einstellung würde den anderen ändern, es zu entsprechen. In diesem Fall Square versagt die Liskov Substitution Test mit Rectangle und die Abstraktion Square des Habens erben von Rectangle ein schlechtes ist.

Y'all sollte das andere unschätzbare Besuche SOLID Prinzipien Motivational Poster .

LSP betrifft Invarianten.

Das klassische Beispiel durch die folgenden Pseudo-Code Erklärung gegeben wird (Implementierungen weggelassen):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

Jetzt haben wir ein Problem, obwohl die Schnittstelle übereinstimmt. Der Grund dafür ist, dass wir Invarianten verletzt haben von der mathematischen Definition der Quadrate und Rechtecke ergeben. Die Art und Weise Getter und Setter Arbeit sollte ein Rectangle erfüllen die folgende Invariante:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

Dies ist jedoch unveränderlich muss durch eine korrekte Umsetzung der Square verletzt werden, daher ist es kein gültiger Ersatz von Rectangle ist.

  

Ersetzbarkeit ist ein Prinzip in der objektorientierten Programmierung die besagt, dass in einem Computerprogramm, wenn S ein Subtyp von T ist, dann Objekte vom Typ T mit Objekten des Typs S ersetzt werden kann

Lassen Sie uns ein einfaches Beispiel in Java tun:

Bad Beispiel

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

Die Ente kann fliegen, weil es ein Vogel ist, aber was ist das:

public class Ostrich extends Bird{}

Strauß ist ein Vogel, aber es kann nicht fliegen, Strauß-Klasse ist ein Subtyp der Klasse Vogel, aber es kann die Fliege Methode nicht verwenden, das bedeutet, dass wir LSP Prinzip brechen.

Gutes Beispiel

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

Robert Martin hat eine ausgezeichnete Papier auf dem Liskov Substitutionsprinzip . Er diskutiert subtil und nicht so subtile Weise, in der das Prinzip verletzt werden kann.

Einige relevanten Teile des Papiers (beachten Sie, dass das zweite Beispiel stark kondensiert wird):

  

Ein einfaches Beispiel einer Verletzung von LSP

     

Eine der auffälligsten Verletzungen dieses Prinzips ist die Verwendung von C ++   Run-Time Type Information (RTTI), eine Funktion auszuwählen, basierend auf dem   Typ eines Objekts. das heißt:.

     
void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}
     

Klar, dass die DrawShape Funktion ist schlecht ausgebildet. Es muss wissen   jede mögliche Ableitung der Shape Klasse und muss geändert werden   wenn neue Derivate von Shape geschaffen werden. Tatsächlich sehen viele die Struktur dieser Funktion als Anathema Oriented Design-Objekt.

     

Quadrat und Rechteck, eine subtilere Verletzung.

     

Es gibt jedoch auch andere, viel subtile, Möglichkeiten, die LSP zu verletzen.   Betrachten Sie eine Anwendung, die die Rectangle-Klasse verwendet, wie beschrieben   unten:

     
class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};
     

[...] Stellen Sie sich vor, dass ein Tag die Benutzer die Möglichkeit, verlangen zu manipulieren   Quadrate zusätzlich zu Rechtecke. [...]

     

Natürlich ist ein Quadrat ein Rechteck für alle normalen Absichten und Zwecke.   Da die ISA Beziehung hält, ist es logisch, die Square zu modellieren   Klasse von Rectangle abgeleitet wird. [...]

     

Square werden die SetWidth und SetHeight Funktionen erben. Diese   Funktionen sind völlig ungeeignet für eine Square, da die Breite und   Höhe eines Platzes sind identisch. Dies sollte ein wesentlicher Anhaltspunkt sein   , dass es ein Problem mit dem Design. Allerdings gibt es eine Möglichkeit,   umgeht das Problem. Wir konnten außer Kraft setzen SetWidth und SetHeight [...]

     

Aber betrachten Sie die folgende Funktion:

     
void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}
     

Wenn passieren wir einen Verweis auf ein Square Objekt in dieser Funktion, die   Square Objekt beschädigt werden, da die Höhe nicht verändert werden.   Dies ist ein klarer Verstoß gegen LSP. Die Funktion funktioniert nicht für   Derivate ihrer Argumente.

     

[...]

LSP ist erforderlich, wenn ein Code denkt, es ist die Methoden eines Typs T Aufruf, und rufen Sie können unwissentlich die Methoden eines Typs S, wo S extends T (dh S erbt, leitet sich aus, oder ist ein Subtyp, der übergeordneter Typ T ).

Beispiel geschieht dies, wenn eine Funktion mit einem Eingabeparameter vom Typ T, aufgerufen wird (d.h. aufgerufen) mit einem Argumente Wert vom Typ S. Oder wo eine Kennung vom Typ T wird ein Wert vom Typ S zugeordnet.

val id : T = new S() // id thinks it's a T, but is a S

LSP erfordert die Erwartungen (das heißt Invarianten) für Methoden des Typs T (z Rectangle), nicht verletzt werden, wenn die Methoden des Typs S (z Square) anstelle genannt werden.

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Auch ein Typ mit unveränderlichen Felder hat noch Invarianten, z.B. die unveränderlich Rectangle Setter erwarten Dimensionen unabhängig geändert werden, aber die unveränderlich Platz Setter verletzen diese Erwartung.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP erfordert, daß jedes Verfahren des Subtyps S kontravarianten Eingangsparameter (S) und einen Ausgang kovarianten haben müssen.

kontra bedeutet die Varianz ist im Gegensatz zu der Richtung der Vererbung, dh die Art Si, jeden Eingangsparameters jeder Methode des Subtyps S, muss gleich oder a Supertyp von der Art sein, Ti des entsprechenden Eingangsparameter des entsprechenden Verfahrens des Supertyp T.

Kovarianz bedeutet, die Varianz in der gleichen Richtung der Vererbung, dh die Art So, der Ausgang jedes Verfahren des Subtyps S, muss gleich oder a Subtyp des Typs To sein der entsprechende Ausgang des entsprechenden Verfahrens des Supertyp T.

Dies liegt daran, wenn der Anrufer denkt, dass es eine Art T hat, denkt, dass es ein Verfahren zur T anruft, dann liefert es Argument (e) des Typs Ti und ordnet die Ausgabe an den Typ To. Wenn es tatsächlich ist das entsprechende Verfahren der S Aufruf dann wird jedes Ti Eingabeargument an einen Si Eingabeparameter zugeordnet, und die So Ausgang mit dem Typ To zugeordnet ist. Wenn also Si nicht kontra w.r.t. waren zu Ti, dann zugewiesen werden, ein Subtyp Xi-die kein Subtyp des würde Si-könnte Ti.

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überliegendes oder gleiche Richtung jeweils an jeden Eingangsparameter oder Ausgang (jede Methode von T), die den Typ des Typ Parameter hat.

Zusätzlich wird für jeden Eingabeparameter oder Ausgangstyp, die eine Funktion hat, erforderlich, die Varianz Richtung umgekehrt wird. Diese Regel wird angewendet rekursiv.


Subtyping angemessen ist wo die Invarianten aufgezählt werden können.

Es gibt viel laufende Forschung auf, wie Invarianten zu modellieren, so dass sie vom Compiler erzwungen werden.

Typestate (siehe Seite 3) erklärt und Zustandsinvarianten orthogonal geben erzwingt. Alternativ können Invarianten von Umwandlung Behauptungen Typen . Zum Beispiel, zu behaupten, dass eine Datei vor dem Schließen sie geöffnet ist, dann File.open () könnte eine Art Openfile zurückgeben, die eine Methode close () enthält, die nicht verfügbar ist in Datei. Ein Tic-Tac-toe-API kann ein weiteres Beispiel der Eingabe des Verwendens Invarianten zur Compile-Zeit zu erzwingen. Das Typsystem kann sogar sein, Turing-complete, zB Scala . Dependently- typisierten Sprachen und Theorembeweisern formalisieren die Modelle höherer Ordnung Typisierung.

Aufgrund der Notwendigkeit für die Semantik abstrakt über Erweiterung , ich gehe davon aus, dass die Eingabe unter Verwendung von Invarianten zu modellieren, dh einheitliche höhere Ordnung denotational Semantik, ist besser als die Typestate. ‚Erweiterung‘ ist die unbeschränkte, permutierte Zusammensetzung unkoordinierter, modular Entwicklung. Da scheint es mir, das Gegenteil von Vereinigung zu sein und damit Grad-of-Freiheit, haben zwei voneinander abhängige Modelle (zB Typen und Typestate) für die gemeinsame Semantik zum Ausdruck, die nicht miteinander für erweiterbare Zusammensetzung vereinigt werden kann . Zum Beispiel Expression Problem -ähnlichen Erweiterung wurde in der Subtypisierung, Funktion Überlastung einheitliche und parametrische Typisierung Domains.

Meine theoretische Position ist, dass für Wissen zu existieren (siehe Abschnitt „Zentralisierungs ist blind und untauglich“), gibt es nie sein ein allgemeines Modell, das 100% ige Abdeckung aller möglichen Invarianten in einer Turing-complete Computersprache erzwingen kann. Für Wissen zu existieren, viel ungeahnte Möglichkeiten vorhanden sind, müssen das heißt Unordnung und Entropie immer größer werden. Dies ist die Entropiekraft. Um alle möglichen Berechnungen einer möglichen Erweiterung zu beweisen, ist a priori alle mögliche Erweiterung zu berechnen.

Aus diesem Grunde ist der Halting Satz vorhanden ist, das heißt, es unentscheidbar ist, ob jedes mögliches Programm in einer Turing-vollständigen Programmiersprache beendet. Es kann nachgewiesen werden, dass einige spezifische Programm beendet (eine, die alle Möglichkeiten definiert und berechnet). Aber es ist unmöglich, zu beweisen, dass alle möglichen Verlängerung dieses Programms beendet, es sei denn, die Möglichkeiten für eine Erweiterung dieses Programm nicht Turing vollständig (beispielsweise über abhängige-Typisierung). Da die grundlegende Voraussetzung für die Turing-Vollständigkeit unbegrenzte Rekursion ist, ist es intuitiv zu verstehen, wie Gödels Unvollständigkeitssätze und Russells Paradox auf Verlängerung.

Eine Interpretation dieser Sätze enthält sie in einem generalisierten konzeptionellen Verständnis der Entropiekraft:

  • Gödels Unvollständigkeitssätze :. Jede formale Theorie, in der alle arithmetischen Wahrheiten bewiesen werden kann, ist inkonsistent
  • Russells Paradox : Jede Mitgliedschaft Regel für einen Satz, der einen Satz enthalten, entweder aufzählt die spezifische Art jedes Elements oder selbst enthält. So setzt entweder nicht unbegrenzt Rekursion verlängert oder sie werden. Zum Beispiel der Satz von allem, was nicht eine Teekanne ist, schließt sich, der sich schließt, der sich schließt, etc .... So eine Regel ist inkonsistent, wenn es (einen Satz enthalten kann und) nicht aufzählen nicht die spezifischen Typen (das heißt erlaubt es, alle nicht näher bezeichnete Typen) und nicht unbegrenzt Erweiterung ermöglichen. Dies ist die Menge von Mengen, die nicht Mitglieder selbst sind. Diese Unfähigkeit, konsequent und vollständig aufgezählt über alle mögliche Erweiterung zu sein, istGödels Unvollständigkeitssätze.
  • Liskov Substitutionsprinzip . Im Allgemeinen ist es ein unentscheidbar Problem, ob eine beliebige Menge der Teilmenge eines anderen ist, das heißt Erbe im Allgemeinen unentscheidbar ist
  • Linsky Referenzierung . Es ist unentscheidbar, was die Berechnung von etwas ist, wenn es beschrieben wird oder wahrgenommen, das heißt Wahrnehmung (Realität) hat keinen absoluten Bezugspunkt
  • Coase-Theorem :. Kein externer Referenzpunkt ist, so dass jede Barriere zu unbegrenzten externen Möglichkeiten fehlschlagen
  • Zweiter Hauptsatz der Thermodynamik : das gesamte Universum (ein geschlossenes System, dh alles) Trends maximale Störung, also maximal unabhängige Möglichkeiten.

Der LSP ist eine Regel über den Vertrag des clases: Wenn eine Basisklasse einen Vertrag erfüllt, dann von den LSP abgeleiteten Klassen müssen auch diesen Vertrag erfüllen

.

In Pseudo-python

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

erfüllt LSP, wenn jedes Mal, wenn Foo auf ein abgeleitetes Objekt aufrufen, gibt es genau die gleichen Ergebnisse wie der Aufruf von Foo auf einem Basisobjekt, solange arg gleich ist.

  

Funktionen, die Zeiger oder Verweise auf Basisklassen verwenden muß in der Lage sein, Objekte von abgeleiteten Klassen zu verwenden, ohne es zu wissen.

Als ich zuerst über LSP lesen, ging ich davon aus, dass dies in einem sehr strengen Sinne gemeint war, im Wesentlichen gleich es Implementierung und typsichere Gießen zu verbinden. Was bedeuten würde, dass LSP entweder gewährleistet ist oder nicht durch die Sprache selbst. Zum Beispiel in diesem strengen Sinne ist ThreeDBoard sicherlich für Vorstand substituierbar, soweit der Compiler geht.

Nach der Lektüre bis mehr auf dem Konzept, obwohl ich fand, dass LSP generell breit interpretiert wird als das.

Kurz gesagt, was es bedeutet, für Client-Code zu „wissen“, dass das Objekt hinter dem Zeiger von einem abgeleiteten Typ ist und nicht der Zeigertyp ist nicht auf Sicherheit zu geben. Die Einhaltung der LSP ist auch prüfbar durch die Objekte tatsächliche Verhalten Sondieren. Das heißt, die Auswirkungen eines Staates und Methodenargumente des Objekts Prüfung auf die Ergebnisse der Methodenaufrufe oder die Arten von Ausnahmen von dem Objekt geworfen.

Gehen wir zurück zum Beispiel wieder, in der Theorie die Vorstands Methoden vorgenommen werden ganz gut auf ThreeDBoard zu arbeiten. In der Praxis jedoch wird es sehr schwierig sein, Unterschiede im Verhalten zu verhindern, dass der Client nicht richtig verarbeiten kann, ohne die Funktionalität humpelnd die ThreeDBoard sollten hinzuzufügen.

Mit diesem Wissen in der Hand, LSP Einhaltung Auswertung kann ein großes Werkzeug sein, zu bestimmen, wann Zusammensetzung der geeignetere Mechanismus zur Erweiterung bestehenden Funktionalität, anstatt Vererbung.

Es gibt eine Checkliste, um festzustellen, ob Sie gegen Liskov verstoßen oder nicht.

  • Wenn Sie gegen einen der folgenden Punkte verstoßen -> verstoßen Sie gegen Liskov.
  • Wenn Sie gegen keines verstoßen, können Sie daraus nichts schließen.

Checkliste:

  • In der abgeleiteten Klasse sollten keine neuen Ausnahmen ausgelöst werden:Wenn Ihre Basisklasse ArgumentNullException ausgelöst hat, durften Ihre Unterklassen nur Ausnahmen vom Typ ArgumentNullException oder alle von ArgumentNullException abgeleiteten Ausnahmen auslösen.Das Auslösen einer IndexOutOfRangeException ist ein Verstoß gegen Liskov.
  • Voraussetzungen können nicht verstärkt werden:Angenommen, Ihre Basisklasse arbeitet mit einem Mitglied int.Jetzt erfordert Ihr Untertyp, dass dieser int positiv ist.Dadurch werden die Voraussetzungen gestärkt, und jetzt ist jeder Code kaputt, der zuvor mit negativen Ints einwandfrei funktionierte.
  • Die Nachbedingungen können nicht abgeschwächt werden:Angenommen, Ihre Basisklasse erfordert, dass alle Verbindungen zur Datenbank geschlossen werden, bevor die Methode zurückgegeben wird.In Ihrer Unterklasse haben Sie diese Methode überschrieben und die Verbindung für eine weitere Wiederverwendung offen gelassen.Sie haben die Nachbedingungen dieser Methode abgeschwächt.
  • Invarianten müssen erhalten bleiben:Der schwierigste und schmerzhafteste Zwang, den es zu erfüllen gilt.Invarianten sind einige Zeit in der Basisklasse verborgen und die einzige Möglichkeit, sie aufzudecken, besteht darin, den Code der Basisklasse zu lesen.Grundsätzlich müssen Sie beim Überschreiben einer Methode darauf achten, dass nach der Ausführung der überschriebenen Methode alles Unveränderliche unverändert bleiben muss.Das Beste, was mir einfällt, ist, diese invarianten Einschränkungen in der Basisklasse durchzusetzen, aber das wäre nicht einfach.
  • Verlaufsbeschränkung:Beim Überschreiben einer Methode ist es Ihnen nicht gestattet, eine nicht änderbare Eigenschaft in der Basisklasse zu ändern.Schauen Sie sich diesen Code an und Sie können sehen, dass „Name“ als nicht änderbar (privater Satz) definiert ist, SubType jedoch eine neue Methode einführt, die eine Änderung (durch Reflektion) ermöglicht:

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

Es gibt noch 2 weitere Artikel: Kontravarianz von Methodenargumenten Und Kovarianz von Rückgabetypen.Aber in C# ist das nicht möglich (ich bin ein C#-Entwickler), daher interessieren sie mich nicht.

Referenz:

Ein wichtiges Beispiel für die verwenden von LSP ist in Software-Test .

Wenn ich eine Klasse A, die eine LSP-konforme Unterklasse von B ist, dann kann ich die Testsuite von B wiederzuverwenden A zu testen.

Zur vollständigen Testunterklasse A, ich brauche wohl noch ein paar Testfälle hinzufügen, aber im Minimum kann ich alle übergeordneten Klasse B Testfälle wiederverwenden.

Eine Art und Weise zu realisieren, ist dies durch den Bau, was McGregor ruft eine „Parallelhierarchie für die Prüfung“: My ATest Klasse von BTest erben. Eine Form der Injektion wird dann benötigt, um den Testfall arbeitet mit Objekten des Typs, um sicherzustellen, A statt vom Typ B (ein einfaches Template-Methode Muster tun werden).

Beachten Sie, dass die Super-Test-Suite für alle Unterklassen Implementierungen Wiederverwendung in der Tat eine Art und Weise zu testen, dass diese Unterklasse Implementierungen ist LSP-konform. So kann man auch argumentieren, dass ein sollte die übergeordnete Klasse Testsuite im Rahmen eines Unterklasse ausgeführt werden.

Siehe auch die Antwort auf die Frage Stackoverflow „ Kann ich eine Reihe von wieder verwendbaren Tests implementieren eine Schnittstelle Implementierung zu testen?

ich denke, jede Art bedeckt, was LSP ist technisch: Sie wollen im Grunde weg zu abstrahieren Lage sein, von Subtyp Details und verwenden geordneten Typen sicher.

So Liskov hat 3 zugrunde liegenden Regeln:

  1. Signatur-Regel: Es gibt eine gültige Implementierung eines jeden Betriebes des Supertypen syntaktisch im Subtyp sein sollte. Etwas ein Compiler wird in der Lage sein, für Sie zu überprüfen. Es gibt eine kleine Regel über weniger Ausnahmen werfen und mindestens so zugänglich wie die übergeordneten Typs Methoden zu sein.

  2. Methoden Regel:. Die Durchführung dieser Operationen ist semantisch Ton

    • Schwächere Voraussetzungen: Die Subtyp-Funktionen sollten zumindest das, was der Supertyp als Eingabe nahm, wenn nicht mehr.
    • Stronger Nachbedingungen: Sie sollten die übergeordnete Typ Methoden erzeugen eine Teilmenge der Ausgabe.
  3. Eigenschaften Regel: Das geht über einzelne Funktionsaufrufe.

    • Invarianten: Dinge, die immer wahr sind, müssen treu bleiben. Z.B. Größe eines Sets ist nie negativ.
    • Evolutionary Eigenschaften: In der Regel etwas mit Unveränderlichkeit oder die Art von Zuständen zu tun kann das Objekt in oder vielleicht nur das Objekt wächst und nie schrumpft, so dass die Subtyp Methoden sollten es nicht machen..

All diese Eigenschaften müssen erhalten bleiben und die zusätzliche Subtyp Funktionalität sollte nicht übergeordnete Typ Eigenschaften verletzt.

Wenn diese drei Dinge gesorgt, Sie haben von dem darunterliegenden Material abstrahiert und Sie schreiben, lose gekoppelten Code.

Quelle: Programmentwicklung in Java - Barbara Liskov

Lange Geschichte kurz, lassen Sie uns Rechtecke Rechtecke und Quadrate Quadrate, praktisches Beispiel lassen, wenn eine Elternklasse erstreckt, müssen Sie entweder die genaue Eltern API zu erhalten oder sie zu verlängern.

Angenommen, Sie haben eine Basis ItemsRepository.

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

Und eine Unterklasse erstreckt es:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Dann könnten Sie eine Client haben die Arbeit mit der Basis ItemsRepository API und sich auf sie.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

Der LSP unterbrochen wird, wenn substituierende Eltern Klasse mit einer Unterklasse bricht den Vertrag des API .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Sie können erfahren Sie mehr über wartbare Software in meinem Kurs zu schreiben: https: //www.udemy. com / enterprise-php /

Diese Formulierung des LSP ist viel zu stark:

  

Wenn für jedes Objekt o1 vom Typ S gibt es ein Objekt o2 vom Typ T, so dass für alle P fi Programme definiert in Bezug auf T, das Verhalten von P unverändert ist, wenn o1 für o2 substituiert ist, dann ist S ein Subtyp von T.

was im Grunde bedeutet, dass S ist eine andere, vollständig gekapselt Umsetzung der genau die gleiche Sache wie T. Und ich könnte fett sein und entscheiden, dass die Leistung Teil des Verhaltens von P ...

Also, im Grunde jede Verwendung später Bindung gegen die LSP. Es ist der ganze Sinn der OO ein anderes Verhalten zu erhalten, wenn wir für eine der anderen Art ein Objekt von einer Art ersetzen!

Die Formulierung zitiert von wikipedia besser ist, da sich die Unterkunft im Kontext abhängt und nicht unbedingt umfasst das gesamte Verhalten des Programms.

Einiger Nachtrag: Halle, Ich frage mich, warum jemand über die unveränderlichen, Voraussetzungen und nach Bedingungen der Basisklasse nicht schreiben, die von den abgeleiteten Klassen müssen eingehalten werden. Für eine abgeleitete Klasse D von der Basisklasse B vollständig sustitutable ist, muss Klasse D bestimmten Bedingungen gehorchen:

  • In-Varianten der Basisklasse muß von der abgeleiteten Klasse beibehalten werden
  • Pre-Bedingungen der Basisklasse dürfen nicht von der abgeleiteten Klasse gestärkt werden
  • Post-Bedingungen der Basisklasse nicht von der abgeleiteten Klasse geschwächt werden muss.

So die abgeleitete muss von der Basisklasse auferlegt Kenntnis der oben genannten drei Bedingungen geknüpft werden. Daher sind die Regeln des Subtyping vorge entschiedener. Das heißt, ‚eine‘ Beziehung nur eingehalten werden müssen, wenn bestimmte Regeln durch den Subtyp eingehalten werden. Diese Regeln, die in Form von Invarianten, precoditions und Nachbedingung, sollten von einem formalen ‚ Design Vertrag ‘.

Weitere Diskussionen zu diesem in meinem Blog: Liskov Substitutionsprinzip

In einem sehr einfachen Satz, können wir sagen:

Das Kind Klasse darf nicht seine Basisklasse Eigenschaften verletzen. Es muss damit fähig sein. Wir können sagen, es ist gleich wie Subtyping.

Ich sehe Rechtecke und Quadrate in jeder Antwort, und wie die LSP verletzen.

Ich mag zeigen, wie das LSP kann mit einem Beispiel der realen Welt angepasst werden:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

Dieser Entwurf entspricht den LSP, weil das Verhalten unabhängig von der Implementierung unverändert bleibt wir wählen verwenden.

Und ja, man kann LSP verletzen in dieser Konfiguration eine einfache Änderung tun wie folgt:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

Nun können die Subtypen nicht die gleiche Art und Weise verwendet werden, da sie mehr nicht das gleiche Ergebnis.

  

Liskov des Substitutionsprinzip (LSP)

     

Die ganze Zeit haben wir ein Programm-Modul entwerfen und erstellen wir eine Klasse   Hierarchien. Dann erweitern wir einige Klassen zu schaffen einige abgeleitete   Klassen.

     

Wir müssen sicherstellen, dass die neuen abgeleiteten Klassen erweitern nur ohne   die Funktionalität der alten Klassen zu ersetzen. Andernfalls werden die neuen Klassen   kann unerwünschte Wirkungen hervorrufen, wenn sie in bestehenden Programm verwendet werden   Module.

     

Liskov des Substitutionsprinzip besagt, dass wenn ein Programm-Modul ist   eine Basisklasse verwendet wird, dann kann der Verweis auf die Basisklasse sein   mit einer abgeleiteten Klasse ersetzt werden, ohne die Funktionalität zu beeinträchtigen   das Programmmodul.

Beispiel:

Im Folgenden ist das klassische Beispiel für die das Substitutionsprinzip des Liskov verletzt wird. In dem Beispiel werden zwei Klassen verwendet: Rechteck und Quadrat. Nehmen wir an, dass das Rectangle-Objekt irgendwo in der Anwendung verwendet wird. Wir haben die Anwendung erweitern und die Square-Klasse hinzufügen. Die quadratische Klasse wird von einer Fabrik Mustern zurück, basierend auf bestimmten Bedingungen, und wir wissen nicht genau, welche Art von Objekt zurückgegeben werden. Aber wir wissen, es ist ein Rechteck. Wir bekommen das Rechteck-Objekt, die Breite auf 5 und die Höhe auf 10 und die Gegend zu bekommen. Für ein Rechteck mit einer Breite 5 und eine Höhe von 10, sollte die Fläche 50. Statt sein, wird das Ergebnis sein 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}
  

Fazit:

     

Dieses Prinzip ist nur eine Erweiterung des Open Close-Prinzip und   bedeutet, dass wir sicher, dass neue abgeleitete Klassen erweitern machen müssen   die Basisklassen, ohne ihr Verhalten zu ändern.

Siehe auch: Öffnen Schließen Prinzip

Einige ähnliche Konzepte für eine bessere Struktur: Konvention vor Konfiguration

Würde ThreeDBoard in Bezug auf eine Reihe von Board, dass nützlich sein, die Umsetzung?

Vielleicht können Sie Scheiben ThreeDBoard in verschiedenen Ebenen als Vorstand zu behandeln. In diesem Fall Sie abstrahieren eine Schnittstelle (oder eine abstrakte Klasse) mögen für Vorstand für mehrere Implementierungen zu ermöglichen.

In Bezug auf die externe Schnittstelle, Sie könnten einen Vorstand Schnittstelle für Faktor wollen beide TwoDBoard und ThreeDBoard (obwohl keines der oben genannten Methoden passen).

Ein Quadrat ist ein Rechteck, in dem die Breite, die Höhe entspricht. Wenn der Platz zwei verschiedene Größen für die Breite und Höhe legt gegen den Platz invariant. Dies wird durch die Einführung von Nebenwirkungen, um gearbeitet. Aber wenn das Rechteck hat eine setSize (Höhe, Breite) mit Bedingung 0

Daher ist eine veränderbare Quadrat ist kein resizable Rechteck.

Lassen Sie uns sagen, dass wir ein Rechteck in unserem Code verwenden

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

In unserer Geometrie Klasse haben wir gelernt, dass ein Quadrat eine spezielle Art von Rechteck ist, weil seine Breite die gleiche Länge wie seine Höhe ist. Lassen Sie sich eine Square Klasse machen und auf der Grundlage dieser Informationen:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Wenn wir die Rectangle mit Square in unserem ersten Code zu ersetzen, dann wird es brechen:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

Das ist, weil die Square eine neue Voraussetzung hat, die wir nicht in der Rectangle Klasse haben: width == height. Nach LSP sollten die Rectangle Instanzen mit Rectangle Unterklassen Instanzen ersetzbar sein. Dies liegt daran, dass diese Instanzen die Typprüfung für Rectangle Instanzen passieren und so werden sie unerwartete Fehler im Code führen.

Dies war ein Beispiel für die an den Wiki-Artikel . So zusammenzufassen, LSP Verletzung wahrscheinlich Fehler im Code zu einem bestimmten Zeitpunkt führen wird.

Lassen Sie sich in Java zeigen:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Es gibt hier kein Problem, nicht wahr? Ein Auto ist auf jeden Fall eine Transportvorrichtung, und hier können wir sehen, dass es die startEngine () -Methode von ihrer Oberklasse außer Kraft setzt.

Lassen Sie uns eine andere Transporteinrichtung hinzuzufügen:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

Alles ist wie geplant jetzt nicht hin! Ja, ist ein Fahrrad eine Transportvorrichtung, ist es jedoch nicht einen Motor hat und daher das Verfahren startEngine () kann nicht durchgeführt werden.

  

Dies sind die Arten von Problemen, die Verletzung von Liskov Auswechslung   Prinzip führt zu, und sie können die meisten in der Regel durch eine anerkannt werden   Verfahren, die nichts oder sogar tun nicht umgesetzt werden können.

Die Lösung für diese Probleme ist eine korrekte Vererbungshierarchie, und in unserem Fall würden wir das Problem durch Differenzierung Klassen von Transporteinrichtungen mit und ohne Motoren lösen. Auch wenn ein Fahrrad eine Transportvorrichtung ist, hat es keinen Motor. In diesem Beispiel ist unsere Definition von Transportvorrichtung falsch. Es soll keinen Motor hat.

Wir können unsere TransportationDevice Klasse Refactoring wie folgt:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Jetzt können wir TransportationDevice für nicht motorisierte Geräte erweitern.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

Und erweitern TransportationDevice für motorisierte Geräte. Hier ist besser geeignet ist, den Motor Objekt hinzuzufügen.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

So ist unsere Car-Klasse wird mehr spezialisiert, während auf das Liskov Substitutionsprinzip einzuhalten.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

Und unsere Fahrrad-Klasse ist auch in Übereinstimmung mit dem Liskov Substitutionsprinzip.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

Ich ermutige Sie, den Artikel zu lesen: Verletzen Liskov Substitutionsprinzip (LSP) .

Hier finden Sie sich eine Erklärung, was das Liskov Substitutionsprinzip, allgemeine Hinweise sind Sie zu helfen, wenn Sie bereits verletzt hat zu erraten, und ein Beispiel Ansatz, dass Sie Ihre Klassenhierarchie wird dazu beitragen, mehr sicher.

Die deutlichste Erklärung für LSP ich bisher gefunden wurde „Das Liskov Substitutionsprinzip besagt, dass das Objekt einer abgeleiteten Klasse in der Lage sein soll, ein Objekt der Basisklasse zu ersetzen, ohne irgendwelche Fehler im System zu bringen oder das Verhaltens der Modifizierung die Basisklasse "von hier . Der Artikel gibt Codebeispiel für LSP verletzt und Festsetzung es.

Liskov Substitutionsprinzip (Von Mark Seemann Buch) heißt es, dass wir in der Lage sein sollten, mit einem anderen Implementierung einer Schnittstelle zu ersetzen, ohne entweder Client oder implementation.It ist dieses Prinzip zu brechen, die Anforderungen, die in der Zukunft auftreten, auch ansprechen kann, wenn wir können sie heute nicht voraussehen.

Wenn wir den Computer von der Wand (Implementation), weder die Steckdose (Interface) noch den Computer (Client) zusammenbricht (in der Tat ziehen, wenn es ein Laptop-Computer ist, kann es sogar auf seinen Batterien für einen Zeitraum laufen von Zeit). Mit Software jedoch erwartet, dass ein Kunde oft ein Dienst zur Verfügung steht. Wenn der Dienst entfernt wurde, erhalten wir eine Nullreferenceexception. Um mit einer solchen Situation zu umgehen, können wir eine Implementierung einer Schnittstelle schaffen, das eine „nichts.“ Dies ist ein Entwurfsmuster als Null-Objekt bekannt ist, [4] und es entspricht in etwa den Computer von der Wand abziehen. Weil wir lose Kopplung verwenden, können wir eine echte Umsetzung mit etwas ersetzen, die nichts tun, ohne Probleme zu verursachen.

LIKOV das Substitutionsprinzip besagt, dass , wenn ein Programm-Modul eine Basisklasse verwendet, dann kann der Verweis auf die Basisklasse mit einer abgeleiteten Klasse ersetzt werden, ohne die Funktionalität des Programmmoduls zu beeinflussen.

Intent -. Abgeleitet Typen müssen vollständig für ihre Basistypen ersetzen kann sein

Beispiel -. Co-Variante Rückgabetypen in Java

LSP sagt, dass ‚‘ Objekte sollten von ihren Subtypen austauschbar sein ‚‘. Auf der anderen Seite, dieses Prinzip verweist auf

  

Child Klassen sollten nie die Eltern class`s Typdefinitionen brechen.

und das folgende Beispiel hilft ein besseres Verständnis der LSP zu haben.

Ohne LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Fixing von LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Lassen Sie mich versuchen, sollten Sie eine Schnittstelle:

interface Planet{
}

Dies wird durch Klasse implementiert:

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

Sie verwenden Erde wie:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Betrachten wir nun eine weitere Klasse, die Erde erstreckt:

class LiveablePlanet extends Earth{
   public function color(){
   }
}

Jetzt nach LSP, sollten Sie in der Lage sein LiveablePlanet der Erde in Position zu verwenden und es soll Ihr System nicht brechen. Wie:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Beispiele genommen von hier

Hier ist ein Auszug aus diesem Beitrag , die verdeutlicht Dinge schön:

[..], um einige Grundsätze zu verstehen, ist es wichtig zu erkennen, wenn sie verletzt worden ist. Dies ist, was ich jetzt tun wird.

Was bedeutet die Verletzung dieses Prinzips bedeuten? Es bedeutet, dass ein Objekt nicht den Vertrag durch ein mit einer Schnittstelle zum Ausdruck Abstraktion auferlegt nicht erfüllt. Mit anderen Worten bedeutet dies, dass Sie Ihre Abstraktionen falsch identifiziert.

Betrachten Sie das folgende Beispiel:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

Ist dies eine Verletzung der LSP? Ja. Dies liegt daran, den Vertrag sagt uns das Konto, dass ein Konto abgezogen werden würde, aber das ist nicht immer der Fall. Also, was soll ich tun, um es zu beheben? Ich ändern Sie einfach den Auftrag:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà, jetzt der Vertrag erfüllt ist.

Diese subtile Verletzung erlegt häufig einen Client mit der Fähigkeit verwendet, um den Unterschied zwischen konkreten Objekten zu erzählen. Zum Beispiel, da der Vertrag des ersten Konto, es wie folgt aussehen könnte:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

Und gegen dieses automatisch das Open-geschlossene Prinzip [das heißt, für Geld Rückzug Bedarf. Denn man weiß nie, was passiert, wenn ein Objekt den Vertrag verstoßen hat nicht genug Geld. Wahrscheinlich gibt es einfach nichts, wahrscheinlich eine Ausnahme ausgelöst wird. Also muss man überprüfen, ob es hasEnoughMoney() -, die nicht Teil einer Schnittstelle ist. Also diese gezwungene Beton-Klasse abhängige Prüfung ist eine OCP Verletzung].

Dieser Punkt befasst sich auch eine falsche Vorstellung, dass ich ziemlich oft über LSP Verletzung auftreten. Er sagt, die „wenn ein Elternteil Verhalten bei einem Kind verändert, dann ist es LSP verletzt.“ Es ist jedoch nicht -. Solange ein Kind nicht seine Eltern Vertrag verstößt

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top