干貨!終于有人把設計模式的 “里式替換原則”講清楚了

前言

我們學習了 SOLID 原則中的單一職責原則和開閉原則。今天,我們再來學習 SOLID 中的 “L” 對應的原則:里式替換原則。

整體上來講,這個設計原則是比較簡單、容易理解和掌握的。今天我主要通過幾個反例,帶你看看,哪些代碼是違反里式替換原則的?我們該如何將它們改造成滿足里式替換原則?除此之外,這條原則從定義上看起來,跟我們之前講過的 “多態(tài)” 有點類似。

如何理解 “里式替換原則”?

里式替換原則的英文翻譯是:Liskov Substitution Principle,縮寫為 LSP。這個原則最早是在 1986 年由 Barbara Liskov 提出,他是這么描述這條原則的:

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

在 1996 年,Robert Martin 在他的 SOLID 原則中,重新描述了這個原則,英文原話是這樣的:

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。

我們綜合兩者的描述,將這條原則用中文描述出來,是這樣的:子類對象object of subtype/derived class)能夠替換程序(program)中父類對象object of base/parent class)出現(xiàn)的任何地方,并且保證原來程序的邏輯行為(behavior)不變及正確性不被破壞。

這么說還是比較抽象,我們通過一個例子來解釋一下。如下代碼中,父類 Transporter 使用 org.apache.http 庫中的 HttpClient 類來傳輸網(wǎng)絡數(shù)據(jù)。子類 SecurityTransporter 繼承父類 Transporter,增加了額外的功能,支持傳輸 appId 和 appToken 安全認證信息。


public class Transporter {

  private HttpClient httpClient;



  public Transporter(HttpClient httpClient) {

    this.httpClient = httpClient;

  }

  public Response sendRequest(Request request) {

    // ...use httpClient to send request

  }

}

public class SecurityTransporter extends Transporter {

  private String appId;

  private String appToken;

  public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {

    super(httpClient);

    this.appId = appId;

    this.appToken = appToken;

  }

  @Override

  public Response sendRequest(Request request) {

    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {

      request.addPayload("app-id", appId);

      request.addPayload("app-token", appToken);

    }

    return super.sendRequest(request);

  }

}

public class Demo {

  public void demoFunction(Transporter transporter) {

    Reuqest request = new Request();

    //... 省略設置 request 中數(shù)據(jù)值的代碼...

    Response response = transporter.sendRequest(request);

    //... 省略其他邏輯...

  }

}

// 里式替換原則

Demo demo = new Demo();

demo.demofunction(new SecurityTransporter(/* 省略參數(shù) */););

在上面的代碼中,子類 SecurityTransporter 的設計完全符合里式替換原則,可以替換父類出現(xiàn)的任何位置,并且原來代碼的邏輯行為不變且正確性也沒有被破壞。

不過,你可能會有這樣的疑問,剛剛的代碼設計不就是簡單利用了面向?qū)ο蟮亩鄳B(tài)特性嗎?多態(tài)和里式替換原則說的是不是一回事呢?從剛剛的例子和定義描述來看,里式替換原則跟多態(tài)看起來確實有點類似,但實際上它們完全是兩回事。為什么這么說呢?

我們還是通過剛才這個例子來解釋一下。不過,我們需要對 SecurityTransporter 類中 sendRequest () 函數(shù)稍加改造一下。改造前,如果 appId 或者 appToken 沒有設置,我們就不做校驗;改造后,如果 appId 或者 appToken 沒有設置,則直接拋出 NoAuthorizationRuntimeException 未授權異常。改造前后的代碼對比如下所示:

// 改造前:

public class SecurityTransporter extends Transporter {

  //... 省略其他代碼..

  @Override

  public Response sendRequest(Request request) {

    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {

      request.addPayload("app-id", appId);

      request.addPayload("app-token", appToken);

    }

    return super.sendRequest(request);

  }

}

// 改造后:

public class SecurityTransporter extends Transporter {

  //... 省略其他代碼..

  @Override

  public Response sendRequest(Request request) {

    if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {

      throw new NoAuthorizationRuntimeException(...);

    }

    request.addPayload("app-id", appId);

    request.addPayload("app-token", appToken);

    return super.sendRequest(request);

  }

}

在改造之后的代碼中,如果傳遞進 demoFunction () 函數(shù)的是父類 Transporter 對象,那 demoFunction () 函數(shù)并不會有異常拋出,但如果傳遞給 demoFunction ()函數(shù)的是子類 SecurityTransporter 對象,那 demoFunction () 有可能會有異常拋出。盡管代碼中拋出的是運行時異常(Runtime Exception),我們可以不在代碼中顯式地捕獲處理,但子類替換父類傳遞進 demoFunction 函數(shù)之后,整個程序的邏輯行為有了改變。

雖然改造之后的代碼仍然可以通過 Java 的多態(tài)語法,動態(tài)地用子類 SecurityTransporter 來替換父類 Transporter,也并不會導致程序編譯或者運行報錯。但是,從設計思路上來講,SecurityTransporter 的設計是不符合里式替換原則的。

回顧

里式替換原則是用來指導,繼承關系中子類該如何設計的一個原則。理解里式替換原則,最核心的就是理解 design by contract,按照協(xié)議來設計” 這幾個字。父類定義了函數(shù)的 “約定”(或者叫協(xié)議),那子類可以改變函數(shù)的內(nèi)部實現(xiàn)邏輯,但不能改變函數(shù)原有的 “約定”。

這里的約定包括:函數(shù)聲明要實現(xiàn)的功能;對輸入、輸出異常的約定;甚至包括注釋中所羅列的任何特殊說明。

更多java原創(chuàng)閱讀:https://javawu.com

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容