OO設計原則 -- Liskov Substitution Principle: 里氏替換原則(LSP)
概要
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之類對子類型別進行判斷的條件。
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語言對其加以重寫,並忽略了某些細節只列出下面的精要
部分來說明 里氏替換原則 對類的繼承上的約束。
//程式碼: 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繼承。
這種繼承關係有什麼問題嗎?
假如已有的系統中存在以下既有的業務邏輯程式碼:
void g(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
if (r.getWidth() * r.getHeight() != 20) {
throw new RuntimeException();
}
}
則對應於擴充套件類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