1. 程式人生 > >六大設計原則(二)LSP裏氏替換原則

六大設計原則(二)LSP裏氏替換原則

參數類型 錯誤 所有 類方法 放大 狙擊手 strong pub tps

裏氏替換原則LSP(Liskov Subsituation Principle)

裏氏替換原則定義

所有父類出現的地方可以使用子類替換並不會出現錯誤或異常,但是反之子類出現的地方不一定能用父類替換。

LSP的四層含義

  • 子類必須完全實現父類的方法
  • 子類可以自己的個性(屬性和方法)
  • 覆蓋或實現父類的方法時輸入參數可以被放大
  • 覆蓋或實現父類的方法時輸出結果可以被縮小

LSP的定義含義1——子類必須完全實現父類的方法

假設如下場景:定義一個槍支抽象類,一個場景類,三個槍支實現類,一個士兵類。此處,三個槍支完全實現了父類的方法。

關聯關系:實線箭頭
泛化關系:實線空心箭頭(繼承關系)
依賴關系:虛線箭頭(使用關系)一個類需要另一個類的協助

技術分享圖片
抽象槍支類:射擊功能

package des.lsp;

/**
 * 抽象類 槍支
 */
abstract class AbstractGun {
    //射擊功能
  public abstract void shoot();
}

子類實現

package des.lsp;

/**
 * 手槍
 */
public class HandGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("手槍可以射擊");
    }
}
package des.lsp;

/**
 * 手槍
 */
public class MachineGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("步槍可以射擊");
    }
}
package des.lsp;

/**
 * 步槍
 */
public class Rifle extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("步槍可以射擊");
    }
}

士兵類:士兵類使用的是抽象槍支類,具體的需要在場景類中指定。

類中調用其他類必須使用父類或接口,若不能使用則其實已經違背了LSP原則。

package des.lsp;

public class Soldier {
    private AbstractGun gun;
    public void setGun(AbstractGun _gun){
        this.gun = _gun;
    };
    public void killEnemy(){
        System.out.print("士兵開始殺人...");
        gun.shoot();
    }

}

場景類

package des.lsp;

public class Client {
    public static void main(String[] args) {
        // write your code here
        Soldier s = new Soldier();
        s.setGun(new Rifle());
        s.killEnemy();
    }
}

如果加入一個玩具槍類,即玩具槍類同樣繼承抽象槍支類,此時就會存在子類不能實現槍支類方法的情況,因為玩具槍和槍最本質的區別是玩具槍不能射擊的,是無法殺死人的。但是,玩具槍的其他屬性,比如顏色等一些屬性可以委托抽象槍支類進行處理。

如果子類不能完整的實現父類的方法或者父類某些方法在子類中發生了畸變,則應該斷開父子關系采用依賴、組合、聚集等關系來代替原有的繼承。

玩具槍繼承槍支抽象類的情況:射擊方法不能被實現,如果實現裏面具體邏輯為空則毫無意義,即正常情況下不能實現父類的shoot方法,shoot方法必須去掉,從LSP來看如果去掉,則違背了LSP的第一個原則:子類必須實現父類方法。(代碼層面來看如果去掉則會報錯)

package des.lsp;

public class ToyGun extends  AbstractGun {
    @Override
    public void shoot() {
        //此方法不能實現,玩具槍不能射擊
    }
}

解決方法:單獨建立一個抽象類玩具類,把與槍支共有的如聲音、顏色交給抽象槍支類處理,而玩具槍所特有的玩具類的屬性交給抽象玩具類處理,玩具槍類實現玩具抽象類
技術分享圖片

LSP的定義含義2——子類可以含有自己的特性

如圖引入,步槍的實現類即步槍由不同的型號。AUG:狙擊槍可以由望遠鏡功能zoomOut方法。

技術分享圖片
此處Snipper是狙擊手類,狙擊手與狙擊槍是密不可分,屬於組合關系,所以狙擊手類直接使用子類AUG。

package des.lsp;
//狙擊槍
public class AUG extends Rifle {
    //狙擊槍特有功能
    public void zoomOut(){
        System.out.print("通過望遠鏡觀察敵人...");
    }

    @Override
    public void shoot() {
        System.out.print("AUG射擊敵人...");
    }
}
package des.lsp;
//狙擊手
public class Snipper {
    //此處傳入參數為子類,組合關系
    public void killEnemy(AUG aug){
        //觀察
        aug.zoomOut();
        //射擊
        aug.shoot();
    }
}
package des.lsp;

public class Client {
    public static void main(String[] args) {
        
        Snipper s = new Snipper();
        s.killEnemy(new AUG());
    }
}

LSP原則:父類不一定能替換子類

package des.lsp;

public class Client {
    public static void main(String[] args) {
        // write your code here
//        Soldier s = new Soldier();
//        s.setGun(new Rifle());
//        s.killEnemy();

        Snipper s = new Snipper();
        s.killEnemy((AUG) new Rifle());//此處用父類代替了子類
    }
}

報錯代碼
技術分享圖片

LSP的定義含義3——覆蓋或實現父類方法時輸入參數可以被放大

假設有如下場景
父類:方法入參<子類方法入參
技術分享圖片
場景類調用:父類調用自己方法。

package des.lsp;

import java.util.HashMap;

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();
    }
}

輸出結果
技術分享圖片
使用裏氏替換原則:把所有父類出現的地方替換為子類

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {
emy((AUG) new Rifle());

        invoker();
    }
}

輸出結果
技術分享圖片
我們的本意是調用子類重載的方法,入參為Map的方法,但實際程序執行是調用的從父類繼承的方法。如果子類的方法中入參的範圍大於父類入參的範圍,則子類代替父類的時候,子類的方法永遠不會執行。
從另外角度來看,假如父類入參的範圍大於子類的入參的範圍,則父類替換子類就未必能存在,這時候很可能會調用子類的方法執行。此句話較為抽象,實際情況如下。
技術分享圖片
父類和子類的代碼如下

public class Father {
    public Collection doSomething(Map map){
        System.out.print("父類被執行...");
        return map.values();
    }
}
public class Son extends Father {
    public Collection doSomething(HashMap map) {
       System.out.print("子類執行...");
        return map.values();
    }
}

場景類:調用父類

package des.lsp;

import java.util.HashMap;

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();
    }
}

運行結果:不言而喻,是父類被執行
技術分享圖片
采用LSP後

package des.lsp;

import java.util.HashMap;

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的方法,但是程序執行的時候卻自動為我們執行了子類的方法,此時就會導致混亂。
結論:子類中的方法的輸入參數(前置條件或稱形式參數)必須與父類中的輸入參數一致或者更寬松(範圍更大)。

LSP的定義含義4——覆蓋或實現父類的方法時輸出結果可以被縮小

理解:父類的返回類型為T,子類的返回類型為S,即LSP要求S<= T
此時分為兩種情況

  • 如果時覆寫,子類繼承父類,繼承的方法的入參必然相同,此時傳入參數必須時相同或小於,返回的值必然不能大於父類返回值,這是覆寫的要求。
  • 如果時重載,這時候要求子類重載方法的參數類型或數量不相同,其實就是保證輸入參數寬於或等於父類輸入參數,這時候就保證了子類的方法永遠不會被執行,其實就是含義3。

LSP的目的及理解

  • 增強程序的健壯性
  • 保證即使增加子類,原有的子類仍然可以繼續運行。
  • 從一方面來說,在程序中盡量避免直接使用子類的個性,而是通過父類一步一步的使用子類,否則直接使用子類其實就相當於直接把子類當作父類,這就直接導致父類毫無用途,父類和子類的關系也會顯得沒有必要存在了。

六大設計原則(二)LSP裏氏替換原則