1. 程式人生 > >java設計原則--里氏替換原則

java設計原則--里氏替換原則

JAVA設計原則–里氏替換原則(LSP原則)

為什麼要用里氏替換原則?:

為了優化繼承所帶來的缺點,使得繼承的優點發揮到最大,而同時減少缺點帶來的麻煩。

繼承的優缺點:

優點:
1. 程式碼共享,減少建立類的工作量,每個子類都擁有父類的屬性和方法
2. 提高程式碼的重用性(子類可以使用父類的屬性和方法)
3. 子類可以在父類的基礎上進行拓展(重寫父類的方法,實現自己的邏輯)(很多開源框架的擴充套件介面都是通過繼承父類來實現的)。

缺點:
1. 繼承是具有侵入性的。也就是隻要繼承,就必須擁有父類的所有屬性和方法
2. 降低了子類的靈活性。同強增加了耦合性。(因為子類具有父類的屬性和變數,所以,在修改父類的屬性和方法時,需要考慮子類的修改,而這種修改如果沒有規範,可能需要大段的程式碼重構)

里氏替換原則的規範(定義):

1、所有引用父類的地方都必須能透明地使用其子類的物件。
(只要哪裡使用了父類,那麼他的所有子類也必須能使用,替換子類不會發生任何錯誤或異常,呼叫者不需要知道是子類還是父類。)

2、子類出現的地方,父類不一定能適用。

里氏替換原則規範的含義:

1.子類必須完全實現父類的方法

例: 在平常編寫程式碼時,定義介面,然後編寫實現類,而在呼叫時,直接呼叫介面方法(高層抽象)(其他場景下還有抽象類這種情況),而不去呼叫具體的實現了,其實這裡已經使用了里氏替換原則。

例:士兵射擊的場景—–槍(本篇中的例子均來自<<設計模式之禪>>)

槍有很多種類,具體士兵使用什麼槍,得等到呼叫的時候才知道,所以需要我們對槍進行抽象:

槍支的抽象類:
public abstract class AbstractGun {
     //定義模板射擊方法 具體讓子類去實現
     public abstract void shoot();

     // 槍的形狀
     public void shape() { // 槍的形狀};

     // 槍的聲音
     public void voice( // 槍的聲音);
}
// 手槍
public class Handgun extends AbstractGun {
      // 手槍的射擊方法
@Override public void shoot() { System.out.println("手槍射擊"); } } // 步槍 public class Rifle extends AbstractGun { // 步槍的射擊 @Override public void shoot() { System.out.println("步槍射擊"); } } // 機槍 public class MachineGun extends AbstractGun { // 機槍 @Override public void shoot() { System.out.println("機槍掃射"); } }

有了槍支,還需要士兵去使用(呼叫)槍支:

// 士兵
public class Soldier {
     // 定義士兵的槍支
     private AbstractGun gun;
     // 給士兵槍
     public void setGun(AbstractGun _gun) {
          this.gun = _gun;
     }
     // 射擊敵人
     public void killEnemy() {
          System.out.println("士兵開始殺敵人");
          gun.shoot();
     }
}

注意: 在Soldier類中,呼叫槍類時,呼叫了頂級父類AbstractGun ,這符合了里氏替換原則(LSP):
在類中呼叫其他類時,務必使用父類或介面(高層抽象),如果不能使用父類或介面,則說明類的設計已經違背了原則。

士兵有了,槍支有了,之後就需要在實際中(具體場景)去用具體的槍射擊敵人:

public class client {
     public static void main(String[] args) {
          // 產生士兵
          Soldier soldier = new Soldier();
          // 給士兵槍支 這裡給了步槍 如果要使用其他的槍,傳入其他具體的子類即可
          soldier.setGun(new Rifile);
          // 士兵射擊敵人
          soldier.killEnemy();
     }
}

注意:當出現特殊的子類,並且無法應用在父類的場景下時,應當對子類進行抽象,建立一個獨立的父類,並將槍支的抽象類與特殊的子類的抽象類建立委託關係。

// 因為玩具槍不滿足場景(不能殺敵人),所以得獨立建立父類,然後兩個父類下的子類各自延展
public abstract class AbstractToy {
     // 與槍支的抽象類建立委託關係 將聲音、形狀等等一些特性都委託給AbstractGun處理
     private AbstractGun gun;

     // 委託給AbstractGun處理 
     public void shape() {
         System.out.println("槍的形狀");
         gun.shape();
     };

     // 委託給AbstractGun處理 
     public void voice() {
         System.out.println("槍射擊的聲音");
         gun.voice();
     }
}

public class ToyGun extends AbstractToy {

     // 玩具槍形狀
     super.shape();

     // 玩具槍聲音
     super.shape();
}

注意: 如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關係,採用依賴、聚集、組合等關係替代繼承。

2.子類可以存在屬於自己的屬性和方法

里氏替換原則可以正著用,但是不能反著用(在子類出現的地方,父類未必能使用。)

public class AUG extends Rifile {
    // 狙擊槍帶望遠鏡
    public void zoomOut() {
        System.out.println("通過望遠鏡觀察敵人");
    }

    // 
    public void shoot() {
        System.out.println("AUG射擊。。。。。。");
    }
}

// AUG狙擊手
public class Snipper {
    public void killEnemy(Aug aug) {
        // 觀察敵人
        aug.zoomOut();
        // 開始射擊
        anu.shoot();
    }
}

// 客戶端呼叫(使用子類的場景)
public class Client {
    public static void main(String[] args) {
        Snipper snipper = new Snipper();
        snipper.setRile(new AUG());
        snipper.killEnemy();
    }
}

// 客戶端呼叫(使用父類替代子類)
public class Client {
    public static void main(String[] args) {
        Snipper snipper = new Snipper();
        snipper.setRile((AUG) new Rifle());
        snipper.killEnemy();
    }
}

編寫上面那段程式碼,會發現在執行期間會丟擲java.lang.ClassCastException異常,也就是說,向下轉型是不安全的,從里氏替換這個原則上來看,就是子類出現的地方,父類不一定能使用。

3.重寫或者實現父類的方法時,輸入引數可以被放大

例:

// 返回Collection集合型別
public class Father {
    public Collection doSomething(HashMap map) {
        System.out.println("父類被執行...");
        return map.values();
    }
}

// 子類返回Collection集合型別
public class Son extends Father {
    // 放大輸入引數型別
    public Collection doSomething(Map map) {
        System.out.println("子類被執行");
        return map.values();
    }
}

// 場景類
public class Client {
    public static void invoker() {
        // 父類存在的地方,子類可以替代
        Father father = new Father();
        HashMap map = new HashMap();
        father.doSomething(map);
    }

    public static void main(String[] args) {
        invoker();
    }
}

// 場景類
public class Client {
    public static void invoker() {
        Son son = new Son();
        HashMap map = new HashMap();
        son.doSomething(map);
    }
    public static void main(String[] args) {
        invoker();
    }
}

// 然後會發現,上面兩種場景下執行結果相同。

注意: 在上面例子中,子類和父類的方法名相同,但是引數列表不同(方法引數型別不同),所以,這不是重寫,而是過載,因為繼承是讓子類擁有父類的屬性和方法,所以在子類中,有兩個方法,方法名相同,但是引數列表不同,所以是過載。

父類的引數型別是HashMap,子類的引數型別是Map,說明引數型別的範圍被擴大了,當傳入的引數是HashMap時,會發現子類代替父類執行(因為繼承了父類的方法),而真正的子類方法不會被呼叫。而子類想執行方法,必須重寫或過載父類的方法,這樣做是正確的。

因為如果父類引數的類型範圍大於子類的話,那麼父類出現的地方,子類未必可以使用,可能導致程式出錯。

總結: 子類的引數的範圍型別必須大於等於父類的引數的範圍型別

4.重寫或者實現父類的方法時輸出結果可以被縮小

父類的一個方法的返回值的型別是T,子類的相同方法(過載或重寫)的返回型別是S,那麼按照里氏替換原則,S必須小於等於T,也就是說,要麼S和T是同一個型別,要麼S是T的子類 :

第一種情況: 重寫:

父類和子類的同名方法的輸入引數是相同的,放個方法的範圍值S小於等於T,這個是重寫的要求(這是為了向上轉型;既然子類重寫了父類的方法,有時候就需要用父類物件引用來呼叫子類重寫的方法)

第二種情況:過載

要求方法的輸入引數型別或數量不相同,根據里氏替換原則,就是子類的引數範圍要大於或等於父類的引數範圍,也就是說,你寫的方法是不會被呼叫的。