在這壹點上,說明了OO繼承和日常生活中的繼承的本質區別。例如,企鵝在生物分類系統中被歸類為鳥類。我們模仿這個系統,設計這樣的類和關系。
“bird”類中有壹個方法fly,企鵝自然繼承了這個方法,但是企鵝不會飛,所以我們在企鵝類中覆蓋了fly方法,告訴方法的調用方企鵝不會飛。這是完全合理的。但是,這違反了LSP。企鵝是鳥類的壹個亞綱,但是企鵝不會飛!需要註意的是,這裏的“鳥”不再是生物學上的鳥,而是軟件中的類和抽象。
有人會說,企鵝不會飛很正常,這樣寫的代碼也可以正常編譯,只要給使用這個類的客戶代碼加壹個判斷就可以了。但這就是問題所在!首先,客戶端代碼和企鵝代碼很可能不是同時設計的。在如今軟件層層外包的開發模式下,妳連兩個模塊的原點在哪裏都不知道,更別說修改客戶端代碼了。客戶端程序可能是遺留系統的壹部分,並且可能不再被維護。如果因為設計這樣的“企鵝”而不得不修改客戶端代碼,這個責任應該由誰來承擔?(大概是上帝吧,誰告訴他企鵝不會飛。_)“修改客戶代碼”直接違反了OCP,這就是OCP的重要性。違反LSP會使現有設計無法關閉!
修改後的設計如下:
但是這都是關於LSP的嗎?書中舉了壹個經典的例子,又是壹個不合理的例子:正方形不是長方形。這個悖論的細節可以在網上找到,我就不多廢話了。
LSP並沒有提供這個問題的解決方案,只是提出了這樣壹個問題。
於是,工程師開始關註如何保證對象的行為。1988年,B. Meyer提出了契約設計(Design by Contract)理論。DbC從形式化方法中借鑒了壹套方法來保證對象行為和自身狀態,其基本概念很簡單:
先決條件:
在調用每個方法之前,方法都要檢查傳入參數的正確性,正確的方法才能執行,否則就認為調用方違反了約定,不會執行。這被稱為先決條件。
後置條件:
壹旦檢查了前置條件,就必須執行該方法,並且必須保證執行結果符合契約,這就是所謂的後置條件。
不變:
對象本身有壹套檢查自身狀態的檢查條件,以保證對象的本質不變,這叫不變。
這些是單個對象的約束。為了滿足LSP,當存在繼承關系時,子類中方法的前提條件必須與超類中覆蓋的方法的前提條件相同或者更寬松;子類中方法的後置條件必須與超類中方法的後置條件相同或更嚴格。
OO語言中的壹些特性可以說明這個問題:
繼承和重寫超類方法時,子類中方法的可見性必須等於或大於超類中方法的可見性,子類中方法拋出的檢測到的異常只能是超類中對應方法拋出的檢測到的異常的子類。
公共類超類{
公共void方法a()引發異常{}
}
公共類子類擴展超類{
//這種重寫是非法的。
私有void方法a()拋出IOException{}
}
公共類SubClassB擴展超類{
//這個覆蓋是可以的。
public void methodA()拋出FileNotFoundException{}
}
從Java5開始,子類中方法的返回值也可以是對應超類方法返回值的子類。這被稱為“協變”
公共類超類{
公眾號計算(){
返回null
}
}
公共類子類擴展超類{
//只能在Java 5或更高版本中編譯。
公共整數計算(){
返回null
}
}
可以看出,所有這些特性都很好地遵循了LSP。但是DbC呢很遺憾,主流的面向對象語言(不管是動態的還是靜態的)都沒有增加對DbC的支持。但是隨著AOP概念的出現,相信DbC很快就會成為OO語言的重要特性之壹。
壹些題外話:
前陣子《敲響OO時代的鐘聲》和《喪鐘為誰而鳴》兩篇文章,引來無數討論。其中提到了OO語言的很多缺點。事實上,根據LSP和OCP的觀點,無論是靜態類型還是動態類型的系統,只要是OO設計,就應該對對象的行為有嚴格的約束。這種約束不僅體現在方法簽名中,還體現在具體的行為本身中。這才是LSP和DbC的真諦。從這個角度來說,並不能說明“壹切都是對象”的動態語言和“C++,Java”的“接口編程”語言的優缺點,這兩種語言都需要改進。莊哥的DJ思路已經開始引入DbC的概念了。這個還是很值得期待的。^_^
此外,接口的語義也在不斷被OCP、LSP、DbC等概念強化,接口表達了對象行為之間的“契約”關系。而不是簡單的作為壹個語法上的糖來實現多重繼承。