2016-04-02 8 views
0

Ich möchte ein Beispiel mit Rechtecken und Quadraten zeigen:Warum unveränderliche Objekte das Likov Substitutionsprinzip erfüllen können?

class Rectangle { 

private int width; 
private int height; 

public int getWidth() { 
    return width; 
} 

public void setWidth(int width) { 
    this.width = width; 
} 

public int getHeight() { 
    return height; 
} 

public void setHeight(int height) { 
    this.height = height; 
} 

public int area() { 
    return width * height; 
} 

} 

class Square extends Rectangle{ 

@Override 
public void setWidth(int width){ 
    super.setWidth(width); 
    super.setHeight(width); 
} 

@Override 
public void setHeight(int height){ 
    super.setHeight(height); 
    super.setWidth(height); 
} 

} 

public class Use { 

public static void main(String[] args) { 
    Rectangle sq = new Square(); 
    LSPTest(sq); 
} 

public static void LSPTest(Rectangle rec) { 
    rec.setWidth(5); 
    rec.setHeight(4); 

    if (rec.area() == 20) { 
     // do Something 
    } 
} 

} 

Wenn ich eine Instanz von Square statt Rectangle in Verfahren ersetzen LSPTest das Verhalten meines Programms geändert. Dies steht im Gegensatz zum LSP.

Ich habe gehört, dass unveränderliche Objekt erlauben, dieses Problem zu lösen. Aber warum?

Ich habe Beispiel geändert. ich hinzufügen Konstruktor in Rectangle:

public Rectangle(int width, int height) { 
this.width = width; 
this.height = height; 
} 

Dann änderte ich Setter:

public Rectangle setWidth(int width) { 
    return new Rectangle(width, this.height); 
} 

public Rectangle setHeight(int height) { 
    return new Rectangle(this.width, height); 
} 

Nun sieht Square wie:

class Square{ 
public Square() { 

} 

public Square(int width, int height) { 
    super(width, height); 
} 

@Override 
public Rectangle setWidth(int width) { 
    return new Rectangle(width, width); 
} 

@Override 
public Rectangle setHeight(int height) { 
    return new Rectangle(height, height); 
} 

} 

Und hier der Client-Code ist:

public class Use { 

public static void main(String[] args) { 
    Rectangle sq = new Square(4, 4); 
    LSPTest(sq); 
} 

public static void LSPTest(Rectangle rec) { 
    rec = rec.setHeight(5); 

    if (rec.area() == 20) { 
     System.out.println("yes"); 
    } 
} 

} 

Die gleichen Probleme bleiben. Was ist der Unterschied, ob das Objekt selbst geändert wird oder ein neues Objekt zurückgegeben wird? Das Programm verhält sich immer noch anders für die Basisklasse und die Unterklasse.

+2

Was macht Sie denken, unveränderliche Objekte inhärent die LSP satisty? (Hinweis, sie nicht). – jtahlborn

+0

Ich stimme @jtahlborn zu. Und zu versuchen, ein unveränderliches Objekt mit einem Setter zu implementieren, der ein neues Objekt zurückgibt, scheint mir ziemlich verwirrend. –

+1

Ihre quadratische unveränderliche Implementierung ist hier das Problem. Es sollte nicht zwei Argumente in seinem Konstruktor nehmen, sondern nur einen. Und es sollte SetWidth und SetHeight nicht überschreiben. Der Basisklasse setWidth() - Kontrakt soll ein Rectangle mit der gleichen Höhe wie das Original und die angegebene Breite zurückgeben. Es gibt keinen Grund, das in Square zu überschreiben: die Basismethode ist auch für ein Quadrat korrekt: Es gibt kein Quadrat zurück. –

Antwort

2

From here packte ich diese Zitate (Hervorhebung von mir):

Stellen Sie sich auf Ihre Rectangle Basisklasse SetWidth und SetHeight Methoden hatte; das scheint vollkommen logisch zu sein. Wenn jedoch Ihre Rectangle Referenz auf eine Square zeigt, dann macht SetWidth und SetHeight keinen Sinn, da die Einstellung 1 die andere ändern würde, um sie anzupassen. In diesem Fall Square versagt die Liskov Substitution Test mit Rectangle und die Abstraktion mit Square vererben Rectangle ist ein schlecht ein.

... und ...

Was die LSP anzeigt, dass Subtyp Verhalten sollte Basistyp Verhalten übereinstimmen, wie in der Basistyp-Spezifikation definiert. Wenn die Spezifikation des Rechteck-Basistyps besagt, dass Höhe und Breite unabhängig voneinander festgelegt werden können, sagt LSP, dass das Quadrat kein Untertyp des Rechtecks ​​sein darf. Wenn die Rechteck-Spezifikation angibt, dass ein Rechteck unveränderlich ist, kann ein Quadrat ein Untertyp von Rechteck sein. Es geht um Untertypen, die das für den Basistyp angegebene Verhalten beibehalten.

Ich denke, es würde funktionieren, wenn Sie wie einen Konstruktor hatte:

Square(int side){ 
    super(side,side); 
    ... 
} 

Da es keine Möglichkeit gibt, etwas unveränderlich, zu ändern, gibt es keine Setter. Das Quadrat wird immer quadratisch sein.

Aber es sollte möglich sein, eine Beziehung zwischen den beiden zu haben, die LSP nicht verletzt, die Sie auch nicht zwingt, unveränderliche Objekte zu verwenden. Wir gehen nur falsch herum.

In der Mathematik kann ein Quadrat als eine Art Rechteck betrachtet werden. Es ist in der Tat eine spezifischere Art von Rechteck. Naiv, scheint es logisch, Square extends Rectangle zu tun, weil Rechtecke einfach so scheinen super. Der Punkt, eine Unterklasse zu haben, besteht nicht darin, eine schwächere Version einer existierenden Klasse zu erstellen, sondern sie sollte die Funktionalität verbessern.

Warum nicht so etwas wie:

class Square{ 
    void setSides(int side); 
    Boundary getSides(); 
} 
class Rectangle extends Square{ 
    //Overload 
    void setSides(int width, int height); 
    @Override 
    Boundary getSides(); 
} 

ich auch sind, darauf hinzuweisen möchte, dass Setters für Einstellung. Der folgende Code ist schrecklich, weil Sie im Wesentlichen eine Methode erstellt haben, die nicht das tut, was sie sagt.

public Rectangle setWidth(int width) { 
    return new Rectangle(width, this.height); 
} 
0

Das Problem ist mit den Verträge, das heißt die Erwartungen der Programmierer, die Ihren Rectangle verwenden.

Der Vertrag ist, dass Sie eine setWidth(15) tun können und danach getWidth() kehrt 15 bis Sie eine andere setWidth mit einem anderen Wert zu tun.
Aus der Sicht von setHeight bedeutet dies, dass es sich nicht ändern darf height. Setzen Sie diese Gedankenlinie fort, und der Vertrag für einen Setter lautet "Aktualisieren Sie diese Eigenschaft auf den Parameterwert und lassen Sie alle anderen Eigenschaften unverändert". Jetzt

in Square, die neuen invariant getWidth() == getHeight() Kräfte setWidth auch die Höhe eingestellt, und voilà: Der Vertrag von setHeight verletzt wird.

Natürlich können Sie im Vertrag Rectangle.setWidth() ausdrücklich angeben (d. H. Die Methodendokumentation), dass die Breite geändert werden kann, wenn setHeight() aufgerufen wird.
Aber jetzt ist Ihr Vertrag auf setWidth ziemlich nutzlos: Es wird die Breite einstellen, die nach einem Aufruf an setHeight konstant bleiben kann oder nicht, abhängig davon, was eine Unterklasse entscheiden könnte.

Dinge können schlimmer werden. Nehmen Sie an, dass Sie Ihre Rectangle ausrollen, Leute beschweren sich ein wenig über den ungewöhnlichen Vertrag auf den Setter, aber ansonsten ist alles in Ordnung.
Aber jetzt kommt jemand und möchte eine neue Unterklasse hinzufügen, OriginCenteredRectangle. Jetzt muss auch die Breite geändert werden, um x und y zu aktualisieren. Viel Spaß beim Erklären Ihrer Benutzer, dass Sie den Vertrag der Basisklasse ändern mussten, damit eine andere Unterklasse (die nur 10% Ihrer Benutzer benötigen) hinzugefügt werden konnte.

Die Praxis hat gezeigt, dass das OriginCenteredRectangle Problem weit häufiger ist, als die Dummheit dieses Beispiels anzeigt.
Auch hat die Praxis gezeigt, dass Programmierer in der Regel den vollständigen Vertrag nicht kennen, und beginnen, updatefähige Unterklassen zu schreiben, die den Erwartungen entgegenkommen und kleine Fehler verursachen.
So entscheidet sich die meisten Programmiersprachen-Community schließlich, dass Sie Wertklassen benötigen; Nach dem, was ich in C++ und Java gesehen habe, dauert dieser Prozess ein oder zwei Jahrzehnte.

Jetzt mit unveränderlichen Klassen, Ihre Setter plötzlich anders aussehen: void setWidth(width) wird Rectangle setWidth(width). I.e. Wenn Sie keine Setter schreiben, schreiben Sie Funktionen, die ein neues Objekt mit einer anderen Breite zurückgeben.
Und das ist vollkommen akzeptabel in Square: setWidth bleibt unverändert und gibt immer noch eine Rectangle wie es muss. Natürlich möchten Sie, dass eine Funktion ein anderes Quadrat zurückgibt, also fügt Square die Funktion Square Square.setSize(size) hinzu.

Sie wollen immer noch eine MutableRectangle Klasse, nur um Rectangles zu konstruieren, ohne eine neue Kopie erstellen zu müssen - Sie haben Rectangle toRectangle(), die den Konstruktor aufrufen. (Der andere Name für MutableRectangle wäre RectangleBuilder, die eine etwas eingeschränktere empfohlene Verwendung der Klasse beschreibt. Wählen Sie Ihren Stil - persönlich, denke ich MutableRectangle ist in Ordnung, es ist nur, dass die meisten Unterklassen Versuche fehlschlagen, so würde ich darüber nachdenken, es zu machen final.)

Verwandte Themen