設計模式六大設計原則(二):裏式替換原則

1、里氏替換原則的概念

里氏替換由Barbara Liskov女士提出,其給出了兩種定義:數組

  • 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.(若是對每個類型爲S的對象o1,都有類型爲T的對象o2,使得以T定義的全部程序P在全部的對象o1都代換成o2時,程序P的行爲沒有發生變化,那麼類型S是類型T的子類型。)安全

  • Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(全部引用基類的地方必須能透明地使用其子類的對象。)ide

2、里氏替換原則的含義

結合個人理解,我認爲里氏替換有兩層含義:code

(一)對於業務而言,可以運用多態透明的使用子類對象,加強代碼複用

  • 子類必須徹底實現父類的方法,且實現的方法不能破壞父類的職責定義。 由於若破壞了職責定義後,對於經過父類引用操做子類對象的程序來說,會破壞多態的封裝,使得程序普適性下降。反之,則能夠充分發揮多態的優勢,提升代碼的複用性。

(二)父類引用的子類對象能夠安全的替換爲子類引用,加強代碼擴展性

  • 重載父類的方法時輸入參數不能縮小。 在縮小的狀況下,父類引用替換爲子類引用的時候,可能會出現子類並無重寫父類同名同參數列表方法,可是卻調用到了子類的方法。反之,一個程序模塊的功能原本是經過父類引用操做子類對象來實現,但如今又面臨擴展功能,且咱們的需求沒有普適到對全部的子類都擴展這個功能,咱們但願經過重建(參數列表爲父類型的經過重建子類)或修改(代碼內實例引用爲父類型的經過修改)來擴展這個方法,若程序符合里氏替換原則,這種擴展就是安全的。

3、里氏替換規範的行爲——繼承類與實現接口的方式對比

不管採起繼承類或實現接口,咱們都應該遵循里氏替換原則,保證職責定義不被破壞,父類引用能安全的被子類對象替換。那麼這兩種方式,在實際開發中,有什麼須要注意的地方,應該怎麼處理嘞對象

(一)繼承類

繼承類的優勢在於可以實現便捷、直觀的共享代碼,也能實現多態,但繼承是把雙刃劍,也有須要注意的地方:繼承

1.父類內部實現之間依賴需警戒

基類代碼:接口

public class Base {
    private static final int MAX_NUM = 1000;
    private int[] arr = new int[MAX_NUM];
    private int count;
    public void add(int number){
        if(count<MAX_NUM){
            arr[count++] = number;    
        }
    }
    public void addAll(int[] numbers){
        for(int num : numbers){
            add(num);
        }
    }
}

子類代碼:開發

public class Child extends Base {
    
    private long sum;

    @Override
    public void add(int number) {
        super.add(number);
        sum+=number;
    }

    @Override
    public void addAll(int[] numbers) {
        super.addAll(numbers);
        for(int i=0;i<numbers.length;i++){
            sum+=numbers[i];
        }
    }
    
    public long getSum() {
        return sum;
    }
}

基類的add方法和addAll方法用於將數字添加到內部數組中,子類在此基礎上添加了成員變量sum,用於表示數組元素之和。get

public static void main(String[] args) {
    Child c = new Child();
    c.addAll(new int[]{1,2,3});
    System.out.println(c.getSum());
}

指望結果是1+2+3=6,但是結果倒是12。爲何嘞,這是由於子類調用的父類的addAll方法依賴的add方法同時也被子類重寫了,這裏先addALL再本身統計一遍和至關於統計了兩遍和。it

此時若想正確輸出須要咱們把子類的addAll方法修改成:

@Override
public void addAll(int[] numbers) {
    super.addAll(numbers);
}

但是,這樣又會產生新的一個問題,若是父類修改了add方法的實現爲:

public void addAll(int[] numbers){
    for(int num : numbers){
        if(count<MAX_NUM){
            arr[count++] = num;    
        }
    }
}

那麼輸出又會變爲0了。

從這個例子咱們能夠看出: 若是父類內部方法可能存在依賴,重寫方法不只僅改變了被重寫的方法,同時另外一個方法(假設爲A)也致使出現了誤差,此時若按照原有的職責定義去調用父類的A方法,可能會致使出乎意料的結果。而且,若就算子類在編寫時意識到了父類方法間的依賴,修改成正確實現,那麼父類就沒法自由的修改內部實現了。

這個問題產生的緣由在於咱們重寫方法時每每容易只關注父類被重寫方法的職責定義,而容易忽視父類其餘方法是否存在依賴此方法。致使咱們仍是破壞了父類行爲的職責定義,違反了里氏替換原則,其具備必定的隱蔽性。這就要求咱們在編寫子類實現的時候必須注意到其餘方法受沒受影響。同時依賴於內部方法的父類方法也不能隨意修改,若被修改方法依賴的方法在其中一個子類被重寫。那麼就算父類在本類沒有改變職責定義,實現結果並無區別,可是若該子類調用,也有可能致使子類預期職責誤差的風險。

2.繼承關係難以界定

繼承反映的是‘是否是’的關係,假設有兩個類,鳥類有會fly()的方法,此時咱們須要添加一個企鵝類,從常識上來看企鵝應該是鳥類的子類。可是因爲企鵝的個性,他不能飛,此時就產生了矛盾,本來咱們在父類定義了鳥會飛的職責,按照里氏替換原則,咱們企鵝這個子類的fly()方法必須符合職責定義,可是實際上沒法符合,因此就沒法實現繼承,這與常識相違背。

3.存在單繼承限制

繼承只能繼承一個類,相比接口缺乏必定靈活性。

(二)實現接口

實現接口相比繼承就靈活多了,也沒有那麼多弊端,由於接口僅僅包含職責定義,並無包含代碼實現。其優勢在於:

  • 實現多態
  • 同時子類能夠實現多個接口,相比繼承更爲靈活

可是與繼承類的方式相比,也有不足的地方,其不能實現代碼的共享,雖然可以在實現類中經過注入公共類,用公共類實現代碼共享,可是卻沒有繼承便捷,直觀。

(三)建議

  • 不管是繼承類仍是實現接口,都須要按父類職責定義實現方法
  • 優先使用接口+注入而非繼承
  • 運用繼承時,實現父類內部方法時最好不要互相依賴,若須要依賴,可使用final修飾被依賴的方法,由於父類對於子類來講最好是封裝好的,子類不考慮內部實現也能自由的重寫父類方法,同時注意行爲實現的普適性,只實現真正公共的部分。
  • 運用繼承時,子類儘可能不要重寫父類方法,若需重寫也不能破壞父類的職責定義,需瞭解父類具體實現,瞭解父類的方法之間的依賴關係。
相關文章
相關標籤/搜索