2016-04-02 72 views
0

我想說明與長方形和正方形的例子:爲什麼不可變對象允許遵守Liskov替換原則?

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 
    } 
} 

} 

如果我的方法替代的Square而不是Rectangle實例LSPTest我PROGRAMM的行爲將被改變。這與LSP是相反的。

我聽說,不可變對象允許解決這個問題。但爲什麼?

我改變了例子。 我Rectangle添加構造函數:

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

然後,我改變setter方法:

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

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

現在,Square樣子:

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); 
} 

} 

這裏是客戶端代碼:

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"); 
    } 
} 

} 

同樣的問題依然存在。對象本身是否發生更改或返回新對象有什麼區別。該程序對於基類和子類的行爲仍然不同。

+2

是什麼讓你認爲不可變的對象固有地支持LSP? (提示,他們不)。 – jtahlborn

+0

我同意@jtahlborn。試圖用一個返回一個新對象的setter來實現一個不可變對象似乎讓我感到困惑。 –

+1

你的Square不可變的實現是這裏的問題。它的構造函數不應該有兩個參數,而只有一個參數。它不應該覆蓋setWidth和setHeight。基類setWidth()約定是返回一個具有與原始高度和給定寬度相同高度的Rectangle。沒有理由在Square中覆蓋它:base方法對於Square也是正確的:它只是不返回Square。 –

回答

2

From here我抓住這些報價(重點礦山):

想象一下,你對你的Rectangle基類SetWidthSetHeight方法;這似乎完全合乎邏輯。但是,如果您的Rectangle參考指向Square,那麼SetWidthSetHeight沒有意義,因爲設置一個會改變另一個以匹配它。在這種情況下Square失敗Liskov替換測試與RectangleRectangleSquare繼承的抽象是一個一個

......還有......

什麼LSP指出的是,作爲基本類型規範中定義的亞型行爲應符合基本類型的行爲。如果矩形基本類型規範說明可以獨立設置高度和寬度,則LSP表示該方形不能是矩形的子類型。 如果矩形規範說明矩形是不可變的,那麼正方形可以是矩形的子類型。這完全是關於維護爲基類型指定的行爲的子類型。

我想這是可行的,如果你有一個像一個構造函數:

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

因爲沒有辦法改變的東西不變,有沒有制定者。廣場將永遠是方形的。

但它應該有可能有兩個不違反LSP的關係,也不會強制你使用不可變對象。我們只是錯了。

在數學中,正方形可以被認爲是一種矩形。實際上,它是一種更具體的矩形類型。天真地說,做Square extends Rectangle似乎是合乎邏輯的,因爲矩形看起來很像super。但是創建子類的目的不是爲了創建現有類的更弱版本,而是應該增強功能。

爲什麼不能有這樣的:

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

我也想指出的是,二傳手設置。下面的代碼是可怕的,因爲你基本上已經創建了一個方法,它不會做它所說的。

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

的問題是與合同,即使用您Rectangle程序員的期望。

的契約是你可以做一個setWidth(15)事後getWidth()將返回15,直到你做另一個setWidth用不同的值。
setHeight的角度來看,這意味着它不能改變height。繼續這樣的思路,而設置者的合同是「將此屬性更新爲參數值,並且保持所有其他屬性不變」「。

Square

現在,新的不變getWidth() == getHeight()勢力setWidth也設置高度,瞧:中setHeight合同侵犯。

當然,如果調用setHeight(),您可以在Rectangle.setWidth()的合同(即方法文檔)中明確聲明寬度可能會改變。
但現在你的合同setWidth是相當無用的:它將設置寬度,在調用setHeight之後可能保持不變,這取決於子類可能決定做什麼。

事情會變得更糟。假設你推出了你的Rectangle,人們抱怨一些關於二傳手的不尋常的合同,否則一切都很好。
但現在有人來了,想要添加一個新的子類,OriginCenteredRectangle。現在改變寬度也需要更新x和y。爲你的用戶解釋你需要修改基類的契約,以便可以添加另一個子類(只有10%的用戶需要)。

實踐表明,OriginCenteredRectangle問題比這個例子中的愚蠢更常見。
此外,實踐表明,程序員通常不知道完整的合同,並開始編寫面對期望飛行的可更新的子類,從而導致微妙的錯誤。
所以大多數編程語言社區最終決定你需要值類;從我在C++和Java中看到的,這個過程需要十年或兩年。

現在使用不可變類,您的設置器突然顯示不同: void setWidth(width)變爲Rectangle setWidth(width)。即你不寫setter,你寫的函數返回一個寬度不同的新對象。
SquaresetWidth保持不變並且仍然返回Rectangle是完全可以接受的。 當然,你會想要一個函數來取回一個不同的方塊,所以Square增加了功能Square Square.setSize(size)

你仍然想要一個MutableRectangle類來構造Rectangles,而不必創建一個新的副本 - 你將有Rectangle toRectangle()做構造函數調用。 (MutableRectangle的另一個名字應該是RectangleBuilder,它描述了一個更有限的推薦使用類。選擇你的風格 - 我個人認爲MutableRectangle沒問題,只是大多數子類化嘗試都會失敗,所以我會考慮製作它final。)

相關問題