概要
Functions that use pointers or references to base classesmust be able to use objects of derived classes without knowing it.
所有引用基類的地方必須能透明地使用其子類的對象。
即:
◇ 所以使用基類代碼的地方,用派生類代碼替換後,能夠正確的執行動作處理。
◇ 換句話說,如果派生類替換了基類後,不能夠正確執行動作,那麼他們的繼承關係就應該廢除。
換個說法,只有滿足以下2個條件的OO設計纔可被認爲是滿足了LSP原則:
◇ 不應該在代碼中出現if/else之類對子類類型進行判斷的條件。以下代碼就違反了LSP定義。
view plaincopy to clipboardprint?
01.if (obj typeof Class1) {
02. do something
03.} else if (obj typeof Class2) {
04. do something else
05.}
if (obj typeof Class1) {
do something
} else if (obj typeof Class2) {
do something else
}
◇ 子類應當可以替換父類並出現在父類能夠出現的任何地方,
或者說如果我們把代碼中使用基類的地方用它的子類所代替,代碼還能正常工作。
里氏替換原則LSP是使代碼符合開閉原則的一個重要保證。同時LSP體現了:
◇ 類的繼承原則:如果一個繼承類的對象可能會在基類出現的地方出現運行錯誤,
則該子類不應該從該基類繼承,或者說,應該重新設計它們之間的關係。
◇ 動作正確性保證:從另一個側面上保證了符合LSP設計原則的類的擴展不會給已有的系統引入新的錯誤。
類的繼承原則:
Robert C. Martin氏在介紹Liskov Substitution Principle (LSP)的原文裏,舉了Rectangle和Square的例子。
這裏沿用這個例子,但用Java語言對其加以重寫,並忽略了某些細節只列出下面的精要
部分來說明裏氏替換原則對類的繼承上的約束。
view plaincopy to clipboardprint?
01.//代碼:
02.
03.class Rectangle {
04. double width;
05. doubleheight;
06.
07.
08. public double getHeight() {
09. returnheight;
10. }
11. public void setHeight(doubleheight) {
12. this.height = height;
13. }
14. public double getWidth() {
15. returnwidth;
16. }
17. public void setWidth(doublewidth) {
18. this.width = width;
19. }
20.}
21.
22.class Square extends Rectangle {
23. public void setHeight(doubleheight) {
24. super.setHeight(height);
25. super.setWidth(height);
26. }
27.
28. public void setWidth(doublewidth) {
29. super.setHeight(width);
30. super.setWidth(width);
31. }
32.}
//代碼:
class Rectangle {
double width;
doubleheight;
public double getHeight() {
returnheight;
}
public void setHeight(doubleheight) {
this.height = height;
}
public double getWidth() {
returnwidth;
}
public void setWidth(doublewidth) {
this.width = width;
}
}
class Square extends Rectangle {
public void setHeight(doubleheight) {
super.setHeight(height);
super.setWidth(height);
}
public void setWidth(doublewidth) {
super.setHeight(width);
super.setWidth(width);
}
}
這裏Rectangle是基類,Square從Rectangle繼承。
這種繼承關係有什麼問題嗎?
假如已有的系統中存在以下既有的業務邏輯代碼:
view plaincopy to clipboardprint?
01.void g(Rectangle r) {
02. r.setWidth(5);
03. r.setHeight(4);
04. if (r.getWidth() * r.getHeight() != 20) {
05. throw new RuntimeException();
06. }
07.}
void g(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
if (r.getWidth() * r.getHeight() != 20) {
throw new RuntimeException();
}
}
則對應於擴展類Square,在調用既有業務邏輯時:
view plaincopy to clipboardprint?
01.Rectangle square = newSquare();
02.g(square);
Rectangle square = newSquare();
g(square);
時會拋出一個RuntimeException異常。這顯然違反了LSP原則。
動作正確性保證:
因爲LSP對子類的約束,所以爲已存在的類做擴展構造一個新的子類時,
根據LSP的定義,不會給已有的系統引入新的錯誤。
Design by Contract
根據Bertrand Meyer氏提出的Design by Contract(DBC:基於合同的設計)概念的描述,
對於類的一個方法,都有一個前提條件以及一個後續條件,前提條件說明方法接受什麼樣的參數數據等,
只有前提條件得到滿足時,這個方法才能被調用;同時後續條件用來說明這個方法完成時的狀態,
如果一個方法的執行會導致這個方法的後續條件不成立,那麼這個方法也不應該正常返回。
現在把前提條件以及後續條件應用到繼承子類中,子類方法應該滿足:
1)前提條件不強於基類.
2)後續條件不弱於基類.
換句話說,通過基類的接口調用一個對象時,用戶只知道基類前提條件以及後續條件。
因此繼承類不得要求用戶提供比基類方法要求的更強的前提條件,
亦即,繼承類方法必須接受任何基類方法能接受的任何條件(參數)。
同樣,繼承類必須順從基類的所有後續條件,亦即,
繼承類方法的行爲和輸出不得違反由基類建立起來的任何約束,不能讓用戶對繼承類方法的輸出感到困惑。
這樣,我們就有了基於合同的LSP,基於合同的LSP是LSP的一種強化。
在很多情況下,在設計初期我們類之間的關係不是很明確,
LSP則給了我們一個判斷和設計類之間關係的基準:需不需要繼承,以及怎樣設計繼承關係。
總結
LSP: 子類必須能夠替換基類。
Subtypes must besubstitutable for their base types.
1. LSP關注的是怎樣良好的使用繼承.
2. 必須要清楚是使用一個Method還是要擴展它,但是絕對不是改變它。
3. LSP清晰的指出,OOD的IS-A關係是就行爲方式而言,
行爲方式是可以進行合理假設的,是客戶程序所依賴的。
4. LSP讓我們得出一個重要的結論:一個模型如果孤立的看,並不具有真正意義的有效性。
模型的有效性只能通過它的客戶程序來表現。必須根據設計的使用者做出的合理假設來審視它。
而假設是難以預測的,直到設計臭味出現的時候才處理它們。
5. 對於LSP的違反也潛在的違反了OCP