## 連載:面向對象葵花寶典:思想、技巧與實踐(32) - LSP原則
LSP是唯一一個以人名命名的設計原則,而且作者還是一個“女博士”?
=============================================================
**LSP**,Liskov?substitution?principle,中文翻譯為“**里氏替換原則**”。
?
這是面向對象原則中唯一一個以人名命名的原則,雖然Liskov在中國的知名度沒有UNIX的幾位巨匠(Kenneth?Thompson、Dennis?Ritchie)、GOF四人幫那么響亮,但查一下資料,你會發現其實Liskov也是非常牛的:2008年圖靈獎獲得者,歷史上第一個女性計算機博士學位獲得者。其詳細資料可以在維基百科上查閱:[http://en.wikipedia.org/wiki/Barbara_Liskov](http://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);
//<span style="color:#ff0000;">如下assert判斷為false,斷言失敗,拋出java.lang.AssertionError</span>
assert( rectangle.getArea() == 20);
}
}
~~~
上面這個樣例同時也給出了一個判斷子類是否符合LSP的取巧的方法,即:針對父類的單元測試用例,傳入子類是否也能夠測試通過。如果測試能夠通過,則說明符合LSP原則,否則就說明不符合LSP原則
- 前言
- (1) - 程序設計思想的發展
- (2) - 面向對象語言發展歷史
- (3) - 面向過程 vs 面向對象
- (4) - 面向對象是瑞士軍刀還是一把錘子?
- (5) - 面向對象迷思:面向對象導致性能下降?
- (6) - 不要說你懂“類”
- (7) - “對象”新解
- (8) - “接口” 詳解
- (9) - “抽象類” 詳解
- (10) - “抽象” 詳解
- (11) - “封裝” 詳解
- (12) - “繼承” 詳解
- (13) - “多態” 詳解
- (14) - 面向對象開發技術流程
- (15) - 需求詳解
- (16) - 需求分析終極目的
- (17) - 需求分析518方法
- (18) - 用例分析
- (19) - 功能點提取
- (20) - 用例圖的陷阱
- (21) - SSD
- (22) - 領域模型
- (23) - 領域建模三字經
- (24) - 設計模型
- (25) - 類模型
- (26) - 類模型三板斧
- (27) - 動態模型設計
- (28) - 設計原則:內聚&耦合
- (29) - 高內聚低耦合
- (30) - SRP原則
- (31) - OCP原則
- (32) - LSP原則
- (33) - ISP原則
- (34) - DIP原則
- (35) - NOP原則
- (36) - 設計原則如何用?
- (37) - 設計模式:瑞士軍刀 or 錘子?
- (38) - 設計模式之道
- (39) - 設計原則 vs 設計模式
- (40) - DECORATOR模式
- (完)- 書籍已經出版