2.2 糾紛不斷,規則壓制
里氏替換原則為良好的繼承定義了一個規范,一句簡單的定義包含了4層含義。
1.子類必須完全實現父類的方法
我們在做系統設計時,經常會定義一個接口或抽象類,然后編碼實現,調用類則直接傳入接口或抽象類,其實這里已經使用了里氏替換原則。我們舉個例子來說明這個原則,大家都打過CS吧,非常經典的FPS類游戲,我們來描述一下里面用到的槍,類圖如圖2-1所示。

圖2-1 CS游戲中的槍支類圖
槍的主要職責是射擊,如何射擊在各個具體的子類中定義,手槍是單發射程比較近,步槍威力大射程遠,機槍用于掃射。在士兵類中定義了一個方法killEnemy,使用槍來殺敵人,具體使用什么槍來殺敵人,調用的時候才知道,AbstractGun類的源程序如代碼清單2-1所示。
代碼清單2-1 槍支的抽象類
public?abstract?class?AbstractGun?{
?????//槍用來干什么的?殺敵!
?????public?abstract?void?shoot();
}
手槍、步槍、機槍的實現類如代碼清單2-2所示。
代碼清單2-2 手槍、步槍、機槍的實現類
public?class?Handgun?extends?AbstractGun?{?
?????//手槍的特點是攜帶方便,射程短
?????@Override
?????public?void?shoot()?{
?????????????System.out.println("手槍射擊...");
?????}
}
public?class?Rifle?extends?AbstractGun{?
?????//步槍的特點是射程遠,威力大
?????public?void?shoot(){
?????????????System.out.println("步槍射擊...");
?????}
}
public?class?MachineGun?extends?AbstractGun{????
?????public?void?shoot(){
?????????????System.out.println("機槍掃射...");
?????}
}
有了槍支,還要有能夠使用這些槍支的士兵,其源程序如代碼清單2-3所示。
代碼清單2-3 士兵的實現類
public?class?Soldier?{
?????//定義士兵的槍支
?????private?AbstractGun?gun;
?????//給士兵一支槍
?????public?void?setGun(AbstractGun?_gun){
?????????????this.gun?=?_gun;?
?????}
?????public?void?killEnemy(){
?????????????System.out.println("士兵開始殺敵人...");
?????????????gun.shoot();
?????}
}
注意粗體部分,定義士兵使用槍來殺敵,但是這把槍是抽象的,具體是手槍還是步槍需要在上戰場前(也就是場景中)前通過setGun方法確定。場景類Client的源代碼如代碼清單2-4所示。
代碼清單2-4 場景類
public?class?Client?{
?????public?static?void?main(String[]?args)?{
?????????????//產生三毛這個士兵
?????????????Soldier?sanMao?=?new?Soldier();
?????????????//給三毛一支槍
?????????????sanMao.setGun(new?Rifle());
?????????????sanMao.killEnemy();
?????}
}
有人,有槍,也有場景,運行結果如下所示。
士兵開始殺敵人...
步槍射擊...
在這個程序中,我們給三毛這個士兵一把步槍,然后就開始殺敵了。如果三毛要使用機槍,當然也可以,直接把sanMao.setGun(new Rifle())修改為sanMao.setGun(new MachineGun())即可,在編寫程序時Solider士兵類根本就不用知道是哪個型號的槍(子類)被傳入。
注意 在類中調用其他類時務必要使用父類或接口,如果不能使用父類或接口,則說明類的設計已經違背了LSP原則。
我們再來想一想,如果我們有一個玩具手槍,該如何定義呢?我們先在類圖2-1上增加一個類ToyGun,然后繼承于AbstractGun類,修改后的類圖如圖2-2所示。

圖2-2 槍支類圖
首先我們想,玩具槍是不能用來射擊的,殺不死人的,這個不應該寫在shoot方法中。新增加的ToyGun的源代碼如代碼清單2-5所示。
代碼清單2-5 玩具槍源代碼
public?class?ToyGun?extends?AbstractGun?{
?????//玩具槍是不能射擊的,但是編譯器又要求實現這個方法,怎么辦?虛構一個唄!
?????@Override
?????public?void?shoot()?{
?????????????//玩具槍不能射擊,這個方法就不實現了
?????}
}
由于引入了新的子類,場景類中也使用了該類,Client稍作修改,源代碼如代碼清單2-6所示。
代碼清單2-6 場景類
public?class?Client?{??????
?????public?static?void?main(String[]?args)?{
?????????????//產生三毛這個士兵
?????????????Soldier?sanMao?=?new?Soldier();
?????????????sanMao.setGun(new?ToyGun());
?????????????sanMao.killEnemy();
?????}
}
修改了粗體部分,把玩具槍傳遞給三毛用來殺敵,代碼運行結果如下所示:
士兵開始殺敵人...
壞了,士兵拿著玩具槍來殺敵人,射不出子彈呀!如果在CS游戲中有這種事情發生,那你就等著被人爆頭吧,然后看著自己凄慘地倒地。在這種情況下,我們發現業務調用類已經出現了問題,正常的業務邏輯已經不能運行,那怎么辦?好辦,有兩種解決辦法:
●?在Soldier類中增加instanceof的判斷,如果是玩具槍,就不用來殺敵人。這個方法可以解決問題,但是你要知道,在程序中,每增加一個類,所有與這個父類有關系的類都必須修改,你覺得可行嗎?如果你的產品出現了這個問題,因為修正了這樣一個Bug,就要求所有與這個父類有關系的類都增加一個判斷,客戶非跳起來跟你干架不可!你還想要客戶忠誠于你嗎?顯然,這個方案被否定了。
●?ToyGun脫離繼承,建立一個獨立的父類,為了實現代碼復用,可以與AbastractGun建立關聯委托關系,如圖2-3所示。

圖2-3 玩具槍與真實槍分離的類圖
例如,可以在AbstractToy中聲明將聲音、形狀都委托給AbstractGun處理,仿真槍嘛,形狀和聲音都要和真實的槍一樣了,然后兩個基類下的子類自由延展,互不影響。
在Java的基礎知識中都會講到繼承,Java的三大特征嘛,封裝、繼承、多態。繼承就是告訴你擁有父類的方法和屬性,然后你就可以重寫父類的方法。按照繼承原則,我們上面的玩具槍繼承AbstractGun是絕對沒有問題的,玩具槍也是槍嘛,但是在具體應用場景中就要考慮下面這個問題了:子類是否能夠完整地實現父類的業務,否則就會出現像上面的拿槍殺敵人時卻發現是把玩具槍的笑話。
注意 如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關系,采用依賴、聚集、組合等關系代替繼承。
2.子類可以有自己的個性
子類當然可以有自己的行為和外觀了,也就是方法和屬性,那這里為什么要再提呢?是因為里氏替換原則可以正著用,但是不能反過來用。在子類出現的地方,父類未必就可以勝任。還是以剛才的關于槍支的例子為例,步槍有幾個比較“響亮”的型號,比如AK47、AUG狙擊步槍等,把這兩個型號的槍引入后的Rifle子類圖如圖2-4所示。

圖2-4 增加AK47和AUG后的Rifle子類圖
很簡單,AUG繼承了Rifle類,狙擊手(Snipper)則直接使用AUG狙擊步槍,源代碼如代碼清單2-7所示。
代碼清單2-7 AUG狙擊槍源碼代碼
public?class?AUG?extends?Rifle?{
?????//狙擊槍都攜帶一個精準的望遠鏡
?????public?void?zoomOut(){
?????????????System.out.println("通過望遠鏡察看敵人...");
?????}
?????public?void?shoot(){
?????????????System.out.println("AUG射擊...");
?????}
}
有狙擊槍就有狙擊手,狙擊手類的源代碼如代碼清單2-8所示。
代碼清單2-8 AUG狙擊手類的源碼代碼
public?class?Snipper?{?????
?????public?void?killEnemy(AUG?aug){
?????????????//首先看看敵人的情況,別殺死敵人,自己也被人干掉
?????????????aug.zoomOut();
?????????????//開始射擊
?????????????aug.shoot();
?????}
}
狙擊手,為什么叫Snipper?Snipe翻譯過來就是鷸,就是“鷸蚌相爭,漁人得利”中的那只鳥,英國貴族到印度打獵,發現這個鷸很聰明,人一靠近就飛走了,沒辦法就開始偽裝、遠程精準射擊,于是乎Snipper就誕生了。
狙擊手使用狙擊槍來殺死敵人,業務場景Client類的源代碼如代碼清單2-9所示。
代碼清單2-9 狙擊手使用AUG殺死敵人
public?class?Client?{??????
?????public?static?void?main(String[]?args)?{
?????????????//產生三毛這個狙擊手
?????????????Snipper?sanMao?=?new?Snipper();
?????????????sanMao.setRifle(new?AUG());
?????????????sanMao.killEnemy();
?????}
}
狙擊手使用G3殺死敵人,運行結果如下所示:
通過望遠鏡察看敵人...
AUG射擊...
在這里,系統直接調用了子類,狙擊手是很依賴槍支的,別說換一個型號的槍了,就是換一個同型號的槍也會影響射擊,所以這里就直接把子類傳遞了進來。這個時候,我們能不能直接使用父類傳遞進來呢?修改一下Client類,如代碼清單2-10所示。
代碼清單2-10 使用父類作為參數
public?class?Client?{??????
?????public?static?void?main(String[]?args)?{
?????????????//產生三毛這個狙擊手
?????????????Snipper?sanMao?=?new?Snipper();
?????????????sanMao.setRifle((AUG)(new?Rifle()));
?????????????sanMao.killEnemy();
?????}
}
顯示是不行的,會在運行期拋出java.lang.ClassCastException異常,這也是大家經常說的向下轉型(downcast)是不安全的,從里氏替換原則來看,就是有子類出現的地方父類未必就可以出現。
3.覆蓋或實現父類的方法時輸入參數可以被放大
方法中的輸入參數稱為前置條件,這是什么意思呢?大家做過Web Service開發就應該知道有一個“契約優先”的原則,也就是先定義出WSDL接口,制定好雙方的開發協議,然后再各自實現。里氏替換原則也要求制定一個契約,就是父類或接口,這種設計方法也叫做Design by Contract(契約設計),與里氏替換原則有著異曲同工之妙。契約制定了,也就同時制定了前置條件和后置條件,前置條件就是你要讓我執行,就必須滿足我的條件;后置條件就是我執行完了需要反饋,標準是什么。這個比較難理解,我們來看一個例子,我們先定義一個Father類,如代碼清單2-11所示。
代碼清單2-11 Father類源代碼
public?class?Father?{??????
?????public?Collection?doSomething(HashMap?map){
?????????????System.out.println("父類被執行...");????
?????????????return?map.values();
?????}
}
這個類非常簡單,就是把HashMap轉換為Collection集合類型,然后再定義一個子類,源代碼如代碼清單2-12所示。
代碼清單2-12 子類源代碼
public?class?Son?extends?Father?{
?????//放大輸入參數類型
?????public?Collection?doSomething(Map?map){
?????????????System.out.println("子類被執行...");
?????????????return?map.values();
?????}
}
請注意粗體部分,與父類的方法名相同,但又不是覆寫(Override)父類的方法。你加個@Override試試看,會報錯的,為什么呢?方法名雖然相同,但方法的輸入參數不同,就不是覆寫,那這是什么呢?是重載(Overload)!不用大驚小怪的,不在一個類就不能是重載了?繼承是什么意思,子類擁有父類的所有屬性和方法,方法名相同,輸入參數類型又不相同,當然是重載了。父類和子類都已經聲明了,場景類的調用如代碼清單2-13所示。
代碼清單2-13 場景類源代碼
public?class?Client?{
?????public?static?void?invoker(){
?????????????//父類存在的地方,子類就應該能夠存在
?????????????Father?f?=?new?Father();
?????????????HashMap?map?=?new?HashMap();
?????????????f.doSomething(map);
?????}
?????public?static?void?main(String[]?args)?{
?????????????invoker();
?????}
}
代碼運行后的結果如下所示:
父類被執行...
根據里氏替換原則,父類出現的地方子類就可以出現,我們把上面的粗體部分修改為子類,如代碼清單2-14所示。
代碼清單2-14 子類替換父類后的源代碼
public?class?Client?{
?????public?static?void?invoker(){
?????????????//父類存在的地方,子類就應該能夠存在
?????????????Son?f?=new?Son();
?????????????HashMap?map?=?new?HashMap();
?????????????f.doSomething(map);
?????}
?????public?static?void?main(String[]?args)?{
?????????????invoker();
?????}
}
運行結果還是一樣,看明白是怎么回事了嗎?父類方法的輸入參數是HashMap類型,子類的輸入參數是Map類型,也就是說子類的輸入參數類型的范圍擴大了,子類代替父類傳遞到調用者中,子類的方法永遠都不會被執行。這是正確的,如果你想讓子類的方法運行,就必須覆寫父類的方法。大家可以這樣想,在一個Invoker類中關聯了一個父類,調用了一個父類的方法,子類可以覆寫這個方法,也可以重載這個方法,前提是要擴大這個前置條件,就是輸入參數的類型寬于父類的類型覆蓋范圍。這樣說可能比較難理解,我們再反過來想一下,如果Father類的輸入參數類型寬于子類的輸入參數類型,會出現什么問題呢?會出現父類存在的地方,子類就未必可以存在,因為一旦把子類作為參數傳入,調用者就很可能進入子類的方法范疇。我們把上面的例子修改一下,擴大父類的前置條件,源代碼如代碼清單2-15所示。
代碼清單2-15 父類的前置條件較大
public?class?Father?{
?????public?Collection?doSomething(Map?map){
?????????????System.out.println("父類被執行...");
?????????????return?map.values();
?????}
}
把父類的前置條件修改為Map類型,我們再修改一下子類方法的輸入參數,相對父類縮小輸入參數的類型范圍,也就是縮小前置條件,源代碼如代碼清單2-16所示。
代碼清單2-16 子類的前置條件較小
public?class?Son?extends?Father?{
?????//縮小輸入參數范圍
?????public?Collection?doSomething(HashMap?map){
?????????????System.out.println("子類被執行...");
?????????????return?map.values();
?????}
}
在父類的前置條件大于子類的前置條件的情況下,業務場景的源代碼如代碼清單2-17所示。
代碼清單2-17 子類的前置條件較小
public?class?Client?{
?????public?static?void?invoker(){
?????????????//有父類的地方就有子類
?????????????Father?f=?new?Father();
?????????????HashMap?map?=?new?HashMap();
?????????????f.doSomething(map);
?????}??
?????public?static?void?main(String[]?args)?{
?????????????invoker();
?????}
}
代碼運行結果如下所示:
父類被執行...
那我們再把里氏替換原則引入進來會有什么問題?有父類的地方子類就可以使用,好,我們把這個Client類修改一下,源代碼如代碼清單2-18所示。
代碼清單2-18 采用里氏替換原則后的業務場景類
public?class?Client?{
?????public?static?void?invoker(){
?????????????//有父類的地方就有子類
?????????????Son?f?=new?Son();
?????????????HashMap?map?=?new?HashMap();
?????????????f.doSomething(map);
?????}
?????public?static?void?main(String[]?args)?{
?????????????invoker();
?????}
}
代碼運行后的結果如下所示:
子類被執行...
完蛋了吧?!子類在沒有覆寫父類的方法的前提下,子類方法被執行了,這會引起業務邏輯混亂,因為在實際應用中父類一般都是抽象類,子類是實現類,你傳遞一個這樣的實現類就會“歪曲”了父類的意圖,引起一堆意想不到的業務邏輯混亂,所以子類中方法的前置條件必須與超類中被覆寫的方法的前置條件相同或者更寬松。
4. 覆寫或實現父類的方法時輸出結果可以被縮小
這是什么意思呢,父類的一個方法的返回值是一個類型T,子類的相同方法(重載或覆寫)的返回值為S,那么里氏替換原則就要求S必須小于等于T,也就是說,要么S和T是同一個類型,要么S是T的子類,為什么呢?分兩種情況,如果是覆寫,父類和子類的同名方法的輸入參數是相同的,兩個方法的范圍值S小于等于T,這是覆寫的要求,這才是重中之重,子類覆寫父類的方法,天經地義。如果是重載,則要求方法的輸入參數類型或數量不相同,在里氏替換原則要求下,就是子類的輸入參數寬于或等于父類的輸入參數,也就是說你寫的這個方法是不會被調用的,參考上面講的前置條件。
采用里氏替換原則的目的就是增強程序的健壯性,版本升級時也可以保持非常好的兼容性。即使增加子類,原有的子類還可以繼續運行。在實際項目中,每個子類對應不同的業務含義,使用父類作為參數,傳遞不同的子類完成不同的業務邏輯,非常完美!
- 前言
- 第一部分 大旗不揮,誰敢沖鋒——6大設計原則全新解讀
- 第1章 單一職責原則
- 1.2 絕殺技,打破你的傳統思維
- 1.3 我單純,所以我快樂
- 1.4 最佳實踐
- 第2章 里氏替換原則
- 2.2 糾紛不斷,規則壓制
- 2.3 最佳實踐
- 第3章 依賴倒置原則
- 3.2 言而無信,你太需要契約
- 3.3 依賴的三種寫法
- 3.4 最佳實踐
- 第4章 接口隔離原則
- 4.2 美女何其多,觀點各不同
- 4.3 保證接口的純潔性
- 4.4 最佳實踐
- 第5章 迪米特法則
- 5.2 我的知識你知道得越少越好
- 5.3 最佳實踐
- 第6章 開閉原則
- 6.2 開閉原則的廬山真面目
- 6.3 為什么要采用開閉原則
- 6.4 如何使用開閉原則
- 6.5 最佳實踐
- 第二部分 真刀實槍 ——23種設計模式完美演繹
- 第7章 單例模式
- 7.2 單例模式的定義
- 7.3 單例模式的應用
- 7.4 單例模式的擴展
- 7.5 最佳實踐
- 第8章 工廠方法模式
- 8.2 工廠方法模式的定義
- 8.3 工廠方法模式的應用
- 8.4 工廠方法模式的擴展
- 8.5 最佳實踐
- 第9章 抽象工廠模式
- 9.2 抽象工廠模式的定義
- 9.3 抽象工廠模式的應用
- 9.4 最佳實踐
- 第10章 模板方法模式
- 10.2 模板方法模式的定義
- 10.3 模板方法模式的應用
- 10.4 模板方法模式的擴展
- 10.5 最佳實踐
- 第11章 建造者模式
- 11.2 建造者模式的定義
- 11.3 建造者模式的應用
- 11.4 建造者模式的擴展
- 11.5 最佳實踐
- 第12章 代理模式
- 12.2 代理模式的定義
- 12.3 代理模式的應用
- 12.4 代理模式的擴展
- 12.5 最佳實踐
- 第13章 原型模式
- 13.2 原型模式的定義
- 13.3 原型模式的應用
- 13.4 原型模式的注意事項
- 13.5 最佳實踐
- 第14章 中介者模式
- 14.2 中介者模式的定義
- 14.3 中介者模式的應用
- 14.4 中介者模式的實際應用
- 14.5 最佳實踐
- 第15章 命令模式
- 15.2 命令模式的定義
- 15.3 命令模式的應用
- 15.4 命令模式的擴展
- 15.5 最佳實踐
- 第16章 責任鏈模式
- 16.2 責任鏈模式的定義
- 16.3 責任鏈模式的應用
- 16.4 最佳實踐
- 第17章 裝飾模式
- 17.2 裝飾模式的定義
- 17.3 裝飾模式應用
- 17.4 最佳實踐
- 第18章 策略模式
- 18.2 策略模式的定義
- 18.3 策略模式的應用
- 18.4 策略模式的擴展
- 18.5 最佳實踐
- 第19章 適配器模式
- 19.2 適配器模式的定義
- 19.3 適配器模式的應用
- 19.4 適配器模式的擴展
- 19.5 最佳實踐
- 第20章 迭代器模式
- 20.2 迭代器模式的定義
- 20.3 迭代器模式的應用
- 20.4 最佳實踐
- 第21章 組合模式
- 21.2 組合模式的定義
- 21.3 組合模式的應用
- 21.4 組合模式的擴展
- 21.5 最佳實踐
- 第22章 觀察者模式
- 22.2 觀察者模式的定義
- 22.3 觀察者模式的應用
- 22.4 觀察者模式的擴展
- 22.5 最佳實踐
- 第23章 門面模式
- 23.2 門面模式的定義
- 23.3 門面模式的應用
- 23.4 門面模式的注意事項
- 23.5 最佳實踐
- 第24章 備忘錄模式
- 24.2 備忘錄模式的定義
- 24.3 備忘錄模式的應用
- 24.4 備忘錄模式的擴展
- 24.5 最佳實踐
- 第25章 訪問者模式
- 25.2 訪問者模式的定義
- 25.3 訪問者模式的應用
- 25.4 訪問者模式的擴展
- 25.5 最佳實踐
- 第26章 狀態模式
- 26.2 狀態模式的定義
- 26.3 狀態模式的應用
- 第27章 解釋器模式
- 27.2 解釋器模式的定義
- 27.3 解釋器模式的應用
- 27.4 最佳實踐
- 第28章 享元模式
- 28.2 享元模式的定義
- 28.3 享元模式的應用
- 28.4 享元模式的擴展
- 28.5 最佳實踐
- 第29章 橋梁模式
- 29.2 橋梁模式的定義
- 29.3 橋梁模式的應用
- 29.4 最佳實踐
- 第三部分 誰的地盤誰做主 ——設計模式PK
- 第30章 創建類模式大PK
- 30.1 工廠方法模式VS建造者模式
- 30.2 抽象工廠模式VS建造者模式
- 第31章 結構類模式大PK
- 31.1 代理模式VS裝飾模式
- 31.2 裝飾模式VS適配器模式
- 第32章 行為類模式大PK
- 32.1 命令模式VS策略模式
- 32.2 策略模式VS狀態模式
- 32.3 觀察者模式VS責任鏈模式
- 第33章 跨戰區PK
- 33.1 策略模式VS橋梁模式
- 33.2 門面模式VS中介者模式
- 33.3 包裝模式群PK
- 第四部分 完美世界 ——設計模式混編
- 第34章 命令模式+責任鏈模式
- 34.2 混編小結
- 第35章 工廠方法模式+策略模式
- 35.2 混編小結
- 第36章 觀察者模式+中介者模式
- 36.2 混編小結
- 第五部分 擴展篇
- 第37章 MVC框架
- 37.2 最佳實踐
- 第38章 新模式
- 38.1 規格模式
- 38.2 對象池模式
- 38.3 雇工模式
- 38.4 黑板模式
- 38.5 空對象模式
- 附錄 23種設計模式彩圖