設計模式原則(2)里氏替換原則

里氏替換原則(LSP、Liskov Substitution Principle):Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it;所有引用基類的地方必須能透明地使用其子類的對象。
哪怕你不知道這個名字,你也會在開發中大量的運用。

在面向對象編程中很多時候會用到繼承,繼承的好處就不必多說了,先來說說這裏帶來的一些“麻煩”吧;繼承是入侵性的,其降低了代碼的靈活性,增強了耦合。
我們知道很難有兩全其美的策略。但我們可以在一定範圍內把優點進行放大,把缺點進行縮小或儘可能的規避。LSP正是一種放大繼承優勢的規則或說是策略。

以下是幾條相關黃金規則

1、子類必須完全實現父類的的方法
相信這條是毫無疑問的了。就比如一個抽象類槍(Gun)帶有一個方法shoot進行射擊,可以有各種槍(手槍、步槍、衝鋒槍)繼承自Gun,那在繼承的時候是不是要重寫shoot方法呢?畢竟不同的槍有不同的射擊方式嘛。
在實際業務中有了槍就會有槍的使用者:士兵(Soldier),在抽象代碼設計中我們知道士兵只要知道手上有槍可以上戰場了,而不需要關注具體是什麼槍。所以對於士兵來說只要傳入Gun對象就ok了。問題來了;如果傳入的是玩具槍呢?那玩具槍可不能殺敵呀,這個時候重寫shoot時就得區別對待了。那可以在Soldier中利用instanceOf對傳入的對象進行判斷嗎?對應的功能是可以實現的。但這樣的話是不是每增加一個Gun的子類都需要改動Soldier代碼呢?
還有一種方法需要反過來思考一下。玩具槍是否是可以歸屬到Gun的類型呢?按我們的業務定義來看,Gun是可以用於殺敵的。按這個點出發,我們得考慮是否要新增一個玩具類了。
這裏有了一條來自秦小波老師的建議:如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關係,採用依賴、聚集、組合等關係代替繼承。

2、子類可以有自己的個性
相信這也是不言而喻的了,子類若沒有自己的個性還要子類幹嘛!
我們知道,在接口設計時很多時候涉及到向上轉型(子類對象傳給父類),這可以大大的減少代碼冗餘,也是LSP的核心所在。那反過來向下轉型是否可以呢?就比如步槍(Rifle)下面衍生一個狙擊槍(Aug)子類,把Aug放在Soldier的傳入參數中,實際測試代碼傳入的是Rifle(當然這裏需要一個強轉)。小夥伴們可能也猜到了結果了:ClassCastException;向下轉型是不安全的,也就是說LSP不能反着用,其中原因之一就是子類有了自己的個性而父類中沒有。

3、子類覆蓋(重載)或實現父類的方法時輸入參數可以放大
先來段代碼熱熱身

class Father{
     public Collection getValues(HashMap map) {
          System.out.println("father running...");
          return map.values();
     }
}
class Son extends Father{
     public Collection getValues(Map map) {
          System.out.println("son running...");
          return map.values();
     }
}
public class T3 {  
     public static void main(String[] args) {
          Father father=new Father();
          HashMap map=new HashMap();
          father.getValues(map);
     }
}

以上代碼的輸出結果是啥?tip:注意看子父類方法的傳入參數!結果是:father running…
問題又來了:如果把測試代碼 Father father=new Father()改成Son father=new Son()呢?結果不變。
以上代碼是重載哦!記得在重載的情況下根據傳入參數會有一個就近匹配原則。對於以上結果也是很清晰的了。

另外一種情況:如果把子父類的傳入參數類型調換一下呢?代碼如下:

class Father{
     public Collection getValues(Map map) {
          System.out.println("father running...");
          return map.values();
     }
}
class Son extends Father{
     public Collection getValues(HashMap map) {
          System.out.println("son running...");
          return map.values();
     }
}
public class T3 {  
     public static void main(String[] args) {
          Father father=new Father();
          HashMap map=new HashMap();
          father.getValues(map);
     }
}

這個時候的輸出依舊是father running…
測試代碼換一種寫法,還是和上面一樣,按LSP,把測試代碼 Father father=new Father()改成Son father=new Son()
結果是son running…
問題出現了!個人不知道這算是算是一種問題,從重載的角度來講,這並沒有問題;但從原本的重寫設計角度來講,子類在未重寫父類方法的情況下,卻調用了子類方法;這直接導致了業務的混亂。也即是說子類中方法的前置條件(傳參類型)必須與超類中被覆寫(重寫)的方法的前置條件相同或者更寬鬆(爲父類傳參類型本身或其傳參類型父類)。

4、覆寫(重寫)或實現父類的方法時輸出結果可以被縮小
這算是LSP的另一種更加傳統的解釋,這也是在面向對象編程中需要遵循的規則。也就是說子類在重寫父類方法時,返回值類型需要爲父類返回值類型本身或其子類。究其根本,和上述第二條規則類似;向上轉型。

參考文獻
秦小波《設計模式之禪 》第二版

發佈了132 篇原創文章 · 獲贊 40 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章