設計模式學習筆記 - 設計原則 - 3.里氏替換原則,它和多態的區別是什么?

前言

今天來學習 SOLID 中的 L:里氏替換原則。它的英文翻譯是 Liskov Substitution Principle,縮寫為 LSP。

英文原話是: Functions that use points of references of base classes must be able to use objects of derived classes without knowing it。

用中文描述,是這樣的:子類對象能夠替換程序中父類對象出現的任何地方,并且保證原來程序的邏輯行為不變及正確性不被破壞。


如何理解“里氏替換原則”

開頭對里氏替換原則的解釋比較抽象,通過一個例子來解釋下。父類 Transporter 使用 org.apach.http 來傳輸網絡數據。子類 SecurityTransporter 繼承父類 Transporter,增加了額外的功能,支持傳輸 appIdappToken 安全認證信息。

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;}@Overridepublic 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) {Request request = new Request();// 省略設置 request中數據的代碼...Response response = transporter.sendRequest(request);// 省略其他邏輯}
}// 里氏替換原則
Demo demo = new Demo();
demo.demoFunction(new SecurityTransporter(/*省略參數*/));

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

你可能會有疑問,剛剛的代碼就是利用了多態,多態和里氏替換原則是不是一回事呢?
其實它們完全是兩回事。

我們還是通過剛剛的例子來說明下。對 SecurityTransporter 類中 sendRequest() 函數稍微改造下。對 appIdappToken 進行校驗,若沒有設置,則拋出異常。改造后的代碼如下所示:

// 改造前
public class SecurityTransporter extends Transporter {// 其他代碼忽略...@Overridepublic 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 {// 其他代碼忽略...@Overridepublic 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() 函數的是父類 Transporter 對象,那不會有溢出拋出,但是如果傳遞的是 SecurityTransporter 對象有可能會拋出異常,子類替換父類傳遞進 demoFunction() 函數之后,整個程序的邏輯行為有了改變。

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

總結一下,雖然從定義描述和代碼實現上來看,多態和里氏替換原則有點類似,但它們關注的角度不同。

  • 多態是面向對象編程的一大特性,也是面向對象編程語言的一種語法。是一種代碼實現的思路。
  • 里氏替換原則是一種設計原則,用來指導繼承關系中子類該如何設計,子類的設計要保證在替換父類的時候,不改變原有程序的邏輯以及不破壞原有程序的正確性。

哪些代碼明顯違背了 LSP?

子類在設計的時候,要遵守父類的行為約定(或者叫協議)。行為約定包括:

  • 函數聲明要實現的功能
  • 對輸入、輸出、異常的約定
  • 甚至包括注釋中所羅列的任何特殊說明。

為了更好的說明,我們舉幾個反例來解釋下。

1.子類違背父類聲明要實現的功能

父類中提供的 sortOrdersByAmount() 訂單排序函數,是按照金額從小到大排序,而子類重寫之后,按照創建日期來給訂單排序。那子類的設計就違背里氏替換原則。

2.子類違背父類對輸入、輸出、異常的約定

在父類中,某個函數約定:運行出錯的時候返回 null;獲取數據為空的時候返回空集合(empty collection)。而子類重載后,運行出錯返回異常,獲取不到數據返回 null。那子類的設計就違背里氏替換原則。

在父類中,某個函數約定,輸入可以是任意整數,但子類實現的時候,只允許輸入正整數,負數就拋出異常,即子類對輸入數據的校驗比父類嚴格,那子類的設計就違背里氏替換原則。

在父類中,某個函數約定,只會拋出 ArgumentNullException 異常,那子類的實現中,只允許排拋出 ArgumentNullException 異常,任何其他異常的拋出,都會導致子類違背里氏替換原則。

3.子類違背父類注釋中所羅列的任何特殊說明

父類定義的 withdraw() 提現函數的注釋是這么寫的:“用戶的提現金額不得超過賬戶余額…”,而之類重寫 withdraw() 函數后,針對 VIP 賬號實現了透支提現的功能,也就是提現金額可以大于賬戶余額,那這個子類的設計也是不符合里氏替換原則的。

以上三種典型的違背歷史替換原則的情況。此外,判斷子類的設計實現是否違背里氏替換原則,還有一個小竅門,就是拿父類的單元測試去驗證子類的代碼。如果某些單元測試運行失敗,就有可能說明,子類的設計實現沒有完全地遵守父類的約定,子類有可能違背了里氏替換原則。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/719708.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/719708.shtml
英文地址,請注明出處:http://en.pswp.cn/news/719708.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python-分享篇-生成仿微信公眾號推廣的個性二維碼(支持動態)

代碼 生成仿微信公眾號推廣的個性二維碼(支持動態)from MyQR import myqr # 要生成動態二維碼,只需要將piture參數和save_name參數設置gif動圖即可 myqr.run(wordshttps://blog.csdn.net/stqer/article/details/135553200, # 指定二維碼包含…

JVM(內存區域劃分)

JVM JVM - Java虛擬機 我們編寫的 Java 程序, 是不能夠被 OS 直接識別的 JVM 充當翻譯官的角色, 負責把我們寫的的 Java 程序 ,翻譯給 OS “聽”, 讓 OS 能夠識別我們所寫的 Java 代碼 JVM 內存區域劃分 JVM 是一個應用程序, 在啟動的時候, 會從 操作系統 申請到一整塊很大的內…

AI-RAN聯盟在MWC24上正式啟動

AI-RAN聯盟在MWC24上正式啟動。它的logo是這個樣的: 2月26日,AI-RAN聯盟(AI-RAN Alliance)在2024年世界移動通信大會(MWC 2024)上成立。創始成員包括亞馬遜云科技、Arm、DeepSig、愛立信、微軟、諾基亞、美…

【dc-dc】AP510X單路低壓差線性恒流芯片

說明 AP510X 是一系列外圍電路簡潔的單路線性 LED 恒 流芯片,適用于 3-60V 電壓范圍的 LED 恒流調光 領域。 AP510X 采用我司專利算法,可以實現高精度的恒 流效果,輸出電流恒流精度≤ 3 %,電源供電工作 范…

【LeetCode】升級打怪之路 Day 11:棧的應用、單調棧

今日題目: Problem 1: 棧的應用 155. 最小棧 | LeetCode20. 有效的括號 | LeetCode150. 逆波蘭表達式求值 | LeetCode Problem 2: 單調棧 496. 下一個更大元素 I739. 每日溫度503. 下一個更大元素 II 目錄 Problem 1:棧 - “先進后出”的應用LC 155. 最…

【Java設計模式】五、建造者模式

文章目錄 1、建造者模式2、案例:共享單車的創建3、其他用途 1、建造者模式 某個對象的構建復雜將復雜的對象的創建 和 屬性賦值所分離,使得同樣的構建過程可以創建不同的表示建造的過程和細節調用者不需要知道,只需要通過構建者去進行操作 …

力扣刷題記錄--463. 島嶼的周長

題目鏈接&#xff1a;463. 島嶼的周長 - 力扣&#xff08;LeetCode&#xff09; 題目描述 我的代碼實現 class Solution {public int islandPerimeter(int[][] grid) { int result0; int rowgrid.length; int colgrid[0].length; for(int i0;i<row;i){for(int j0;j<col…

【EI會議征稿通知】2024年圖像處理與人工智能國際學術會議(ICIPAI2024)

2024年圖像處理與人工智能國際學術會議&#xff08;ICIPAI2024&#xff09; 2024 International Conference on Image Processing and Artificial Intelligence&#xff08;ICIPAI2024&#xff09; 2024年圖像處理與人工智能國際學術會議&#xff08;ICIPAI2024&#xff09;將…

返回靜態數據

在Java項目中&#xff0c;往往不會一直返回某某數據&#xff0c;而是會返回一個靜態頁面&#xff0c;那么&#xff0c;如何正確返回一個靜態頁面呢&#xff1f;&#xff1f; 要想成功的返回一個靜態頁面前提是必須要有一個靜態頁面&#xff1a; <!DOCTYPE html> <ht…

如何讓 JOIN 跑得更快?

JOIN 一直是數據庫性能優化的老大難問題&#xff0c;本來挺快的查詢&#xff0c;一旦涉及了幾個 JOIN&#xff0c;性能就會陡降。而且&#xff0c;參與 JOIN 的表越大越多&#xff0c;性能就越難提上來。 其實&#xff0c;讓 JOIN 跑得快的關鍵是要對 JOIN 分類&#xff0c;分…

Effective Programming 學習筆記

1 基本語句 1.1 斷言 在南溪看來&#xff0c;斷言可以用來有效地確定編程中當前代碼運行的前置條件&#xff0c;尤其是以下情況&#xff1a; 第三方工具庫對輸入數據的依賴&#xff0c;例如&#xff1a;minitouch庫對Android版本的要求

第三百八十一回

文章目錄 1. 概念介紹2. 修改方法 015buttonStyle.png2.1 修改形狀2.2 修改顏色2.3 修改位置 3. 示例代碼4. 內容總結 我們在上一章回中介紹了"如何創建以圖片為背景的頁面"相關的內容&#xff0c;本章回中將介紹如何修改按鈕的形狀.閑話休提&#xff0c;讓我們一起T…

2024年華為OD機試真題-文件緩存系統-Python-OD統一考試(C卷)

題目描述: 請設計一個文件緩存系統,該文件緩存系統可以指定緩存的最大值(單位為字節)。 文件緩存系統有兩種操作:存儲文件(put)和讀取文件(get) 操作命令為put fileName fileSize或者get fileName 存儲文件是把文件放入文件緩存系統中;讀取文件是從文件緩存系統中訪問已存…

06. Nginx進階-Nginx代理服務

proxy代理功能 正向代理 什么是正向代理&#xff1f; 正向代理&#xff08;forward proxy&#xff09;&#xff0c;一個位于客戶端和原始服務器之間的服務器。 工作原理 為了從原始服務器獲取內容&#xff0c;客戶端向代理發送一個請求并指定目標&#xff08;即原始服務器…

為不同文章形式選擇不同的WordPress文章模板

在寫文章的時候選擇不同的文章形式&#xff0c;然后打開文章的時候會調用不同文章形式的模板。比如&#xff0c;文章形式為video &#xff0c;就調用single-video.php模板&#xff0c;其它文章形式類似&#xff0c;可以添加多個文章樣式。 //為不同文章形式的內容添加不同的si…

chatgpt-next-web搭建教程,超低成本部署屬于自己的ChatGPT

隨著AI的應用變廣&#xff0c;各類AI程序已逐漸普及&#xff0c;尤其是在一些日常辦公、學習等與撰寫/翻譯文稿密切相關的場景&#xff0c;大家都希望找到一個適合自己的穩定可靠的ChatGPT軟件來使用。 ChatGPT-Next-Web就是一個很好的選擇。它是一個Github上超人氣的免費開源…

Spring AOP在業務中常見的使用方式

目錄 1、動態代理 1.1、jdk動態代理 1.2、cglib動態代理 1.3、動態代理的好處 2、什么是AOP 2.1、AOP常用術語 2.2、切面的構成 3、使用aspectJ框架實現AOP 3.1、aspectJ簡介 聲明實現類ServiceImpl 聲明切面 3.3、AfterReturning后置通知 切面類代碼 3.4、Aroun…

2核4G云服務器租用價格_2核4G云主機優惠價格_2024年報價

租用2核4G服務器費用價格&#xff0c;2核4G云服務器多少錢一年&#xff1f;1個月費用多少&#xff1f;阿里云2核4G服務器30元3個月、輕量應用服務器2核4G4M帶寬165元一年、企業用戶2核4G5M帶寬199元一年&#xff1b;騰訊云輕量2核4G服務器5M帶寬165元一年、252元15個月、540元三…

Spring IOC在業務中常見的使用方式

目錄 1、什么是IOC 2、java實現創建對象的方式有哪些 3、基于配置文件的di實現 3.1、什么是di 3.2、入門案例 3.3、環境搭建 接口和實現類 ioc配置文件 測試程序 3.4、案例總結 3.5、簡單類型屬性的賦值&#xff08;set注入&#xff09; set注入要求 JavaBean sp…

前端項??件很?,?且??訪問速度慢,如何在前端側提?性能?

1. 網絡優化 減少HTTP請求的數量&#xff0c;可以通過合并CSS和JavaScript文件來實現。使用CDN&#xff08;內容分發網絡&#xff09;來加速靜態資源的加載速度。對圖片進行壓縮&#xff0c;選擇正確的格式&#xff0c;并實現懶加載技術&#xff0c;以減少頁面初次加載時的數據…