連載:麵向對象葵花寶典:思想、技巧與實踐(32) - LSP原則
LSP是唯一一個以人名命名的設計原則,而且作者還是一個“女博士”
=============================================================
LSP,Liskov substitution principle,中文翻譯為“裏氏替換原則”。
這是麵向對象原則中唯一一個以人名命名的原則,雖然Liskov在中國的知名度沒有UNIX的幾位巨匠(Kenneth Thompson、Dennis Ritchie)、GOF四人幫那麼響亮,但查一下資料,你會發現其實Liskov也是非常牛的:2008年圖靈獎獲得者,曆史上第一個女性計算機博士學位獲得者。其詳細資料可以在維基百科上查閱:https://en.wikipedia.org/wiki/Barbara_Liskov
言歸正傳,我們來看看LSP原則到底是怎麼一回事。
LSP最原始的解釋當然來源於Liskov女士了,她在1987年的OOPSLA大會上提出了LSP原則,1988年,她將文章發表在ACM的SIGPLAN Notices雜誌上,其中詳細解釋了LSP原則:
A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra.What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T. |
英文比較長,看起來比較累,我們簡單的翻譯並歸納一下:
1) 子類的對象提供了父類的所有行為,且加上子類額外的一些東西(可以是功能,也可以是屬性);
2) 當程序基於父類實現時,如果將子類替換父類而程序不需要修改,則說明符合LSP原則
雖然我們稍微翻譯和整理了一下,但實際上還是很拗口和難以理解。
幸好還有Martin大師也覺得這個不怎麼通俗易懂,Robert Martin在1996年為《C++ Reporter》寫了一篇題為《The The Liskov Substitution Principle》的文章,解釋如下:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. |
翻譯一下就是:函數使用指向父類的指針或者引用時,必須能夠在不知道子類類型的情況下使用子類的對象。
Martin大師解釋了一下,相對容易理解多了。但Martin大師還不滿足,在2002年,Martin在他出版的《Agile Software Development Principles Patterns and Practices》一書中,又進一步簡化為:
Subtypes must be substitutable for their base types。 |
翻譯一下就是:子類必須能替換成它們的父類。
經過Martin大師的兩次翻譯,我相信LSP原則本身已經解釋得比較容易理解了,但問題的關鍵是:如何滿足LSP原則?或者更通俗的講:什麼情況下子類才能替換父類?
我們知道,對於調用者來說(Liskov解釋中提到的P),和父類交互無非就是兩部分:調用父類的方法、得到父類方法的輸出,中間的處理過程,P是無法知道的。
也就是說,調用者和父類之間的聯係體現在兩方麵:函數輸入,函數輸出。詳細如下圖:
有了這個圖之後,如何做到LSP原則就清晰了:
1) 子類必須實現或者繼承父類所有的公有函數,否則調用者調用了一個父類中有的函數,而子類中沒有,運行時就會出錯;
2) 子類每個函數的輸入參數必須和父類一樣,否則調用父類的代碼不能調用子類;
3) 子類每個函數的輸出(返回值、修改全局變量、插入數據庫、發送網絡數據等)必須不比父類少,否則基於父類的輸出做的處理就沒法完成。
有了這三條原則後,就可以很方便的判斷類設計是否符合LSP原則了。需要注意的是第3條的關鍵是“不比父類少”,也就是說可以比父類多,即:父類的輸出是子類輸出的子集。
有的朋友看到這三條原則可能有點納悶:這三條原則一出,那子類還有什麼區別哦,豈不都是一樣的實現了,那還會有不同的子類麼?
其實如果仔細研究這三條原則,就會發現其中隻是約定了輸入輸出,而並沒有約束中間的處理過程。例如:同樣一個寫數據庫的輸出,A類可以是讀取XML數據然後寫入數據庫,B類可以是從其它數據庫讀取數據然後本地的數據庫,C類可以是通過分析業務日誌得到數據然後寫入數據庫。這3個類的處理過程都不一樣,但最後都寫入數據到數據庫了。
LSP原則最經典的例子就是“長方形和正方形”這個例子。從數學的角度來看,正方形是一種特殊的長方形,但從麵向對象的角度來觀察,正方形並不能作為長方形的一個子類。原因在於對於長方形來說,設定了寬高後,麵積 = 寬 * 高;但對於正方形來說,設定高同時就設定了寬,設定寬就同時設定了高,最後的麵積並不是等於我們設定的 寬 * 高,而是等於最後一次設定的寬或者高的平方。
具體代碼樣例如下:
Rectangle.java
package com.oo.java.principles.lsp; /** * 長方形 */ public class Rectangle { protected int _width; protected int _height; /** * 設定寬 * @param width */ public void setWidth(int width){ this._width = width; } /** * 設定高 * @param height */ public void setHeight(int height){ this._height = height; } /** * 獲取麵積 * @return */ public int getArea(){ return this._width * this._height; } }
Square.java
package com.oo.java.principles.lsp; /** * 正方形 */ public class Square extends Rectangle { /** * 設定“寬”,與長方形不同的是:設定了正方形的寬,同時就設定了正方形的高 */ public void setWidth(int width){ this._width = width; this._height = width; } /** * 設定“高”,與長方形不同的是:設定了正方形的高,同時就設定了正方形的寬 */ public void setHeight(int height){ this._width = height; this._height = height; } }
UnitTester.java
package com.oo.java.principles.lsp;
public class UnitTester {
public static void main(String[] args){
Rectangle rectangle = new Rectangle();
rectangle.setWidth(4);
rectangle.setHeight(5);
//如下assert判斷為true
assert( rectangle.getArea() == 20);
rectangle = new Square();
rectangle.setWidth(4);
rectangle.setHeight(5);
//如下assert判斷為false,斷言失敗,拋出java.lang.AssertionError
assert( rectangle.getArea() == 20);
}
}
上麵這個樣例同時也給出了一個判斷子類是否符合LSP的取巧的方法,即:針對父類的單元測試用例,傳入子類是否也能夠測試通過。如果測試能夠通過,則說明符合LSP原則,否則就說明不符合LSP原則
================================================
轉載請注明出處:https://blog.csdn.net/yunhua_lee/article/details/26807601
================================================
最後更新:2017-04-03 08:26:15