04.里式替換原則介紹
目錄介紹
- 01.問題思考的分析
- 02.里式替換原則描述
- 03.如何理解里式替換原則
- 04.電商案例演變過程
- 05.鳥類飛行演變過程
- 06.里氏替換優(yōu)缺點
- 07.里式替換原則總結
01.問題思考的分析
什么是里氏替換的原則,如何理解這一原則?
有那些場景滿足里氏替換原則?它跟多態(tài)有何區(qū)別?
在面向對象編程中,繼承是一種重要的機制,它允許我們創(chuàng)建一個類(子類)來繼承另一個類(父類)的屬性和行為。子類通過繼承父類,可以重用父類的代碼,并且可以添加或修改一些特定的行為。
然而,當使用繼承時,必須確保子類可以無縫地替換父類,而不會破壞原有的程序功能。這就是里式替換原則的背景。
02.里式替換原則描述
里式替換原則的英文翻譯是: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)出現的任何地方,并且保證原來程序的邏輯行為(behavior)不變及正確性不被破壞。
03.如何理解里式替換原則
里氏替換原則(Liskov Substitution Principle,LSP)是設計模式六大原則之一:
- 子類必須能夠替換父類: 子類對象可以替換父類對象,程序的行為不會發(fā)生變化。
- 保證行為一致性: 子類在擴展父類功能的同時,不能改變父類原有的行為。
通俗地說,如果我們在程序中使用的是一個基類對象,那么在不修改程序的前提下,用它的子類對象替換這個基類對象,程序應該仍然可以正常運行。
04.一個錯誤案例演變
4.1 有缺陷的代碼
假設我們在電商系統(tǒng)中設計了一個支付類Payment,有一個子類CreditCardPayment用于處理信用卡支付:
class Payment {
public void pay(double amount) {
// 支付邏輯
}
}
class CreditCardPayment extends Payment {
@Override
public void pay(double amount) {
if (amount > 1000) {
throw new IllegalArgumentException("信用卡支付金額不能超過1000元");
}
// 信用卡支付邏輯
}
}
此時,如果我們在系統(tǒng)中使用Payment基類對象進行支付:
Payment payment = new CreditCardPayment();
payment.pay(1200);
由于CreditCardPayment類中的邏輯限制,當支付金額超過1000元時會拋出異常。這導致了父類Payment的行為在子類CreditCardPayment中發(fā)生了變化,違反了里氏替換原則。
4.2 遵守里氏替換原則
為了遵循里氏替換原則,我們應該確保子類在擴展父類功能時,保持父類的行為一致性。可以通過在父類中添加必要的約束來確保子類行為的一致性:
class Payment {
public void pay(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("支付金額必須大于0");
}
// 通用支付邏輯
}
}
class CreditCardPayment extends Payment {
@Override
public void pay(double amount) {
super.pay(amount);
// 信用卡支付邏輯
}
}
在這個設計中,CreditCardPayment類繼承了父類Payment的行為,并且在支付邏輯之前調用了super.pay(amount),確保所有支付金額都符合父類的約束。
這樣,無論是使用基類對象還是子類對象,程序的行為都保持一致,遵循了里氏替換原則。
05.鳥類飛行演變過程
5.1 未遵守里氏替換原則
例如:鳥一般都會飛行,如燕子的飛行速度大概是每小時 120 千米。但是新西蘭的幾維鳥由于翅膀退化無法飛行。
假如要設計一個實例,計算這兩種鳥飛行 300 千米要花費的時間。顯然,拿燕子來測試這段代碼,結果正確,能計算出所需要的時間;但拿幾維鳥來測試,結果會發(fā)生“除零異常”或是“無窮大”,明顯不符合預期。
未遵守里氏替換原則:
public class LSPtest {
public static void main(String[] args) {
Bird bird1 = new Swallow();
Bird bird2 = new BrownKiwi();
bird1.setSpeed(120);
bird2.setSpeed(120);
System.out.println("如果飛行300公里:");
try {
System.out.println("燕子將飛行" + bird1.getFlyTime(300) + "小時.");
System.out.println("幾維鳥將飛行" + bird2.getFlyTime(300) + "小時。");
} catch (Exception err) {
System.out.println("發(fā)生錯誤了!");
}
}
}
//鳥類
class Bird {
double flySpeed;
public void setSpeed(double speed) {
flySpeed = speed;
}
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
//燕子類
class Swallow extends Bird {
}
//幾維鳥類
class BrownKiwi extends Bird {
public void setSpeed(double speed) {
flySpeed = 0;
}
}
這個設計存在的問題:
幾維鳥類重寫了鳥類的 setSpeed(double speed) 方法,這違背了里氏替換原則。
燕子和幾維鳥都是鳥類,但是父類抽取的共性有問題,幾維鳥的的飛行不是正常鳥類的功能,需要特殊處理,應該抽取更加共性的功能。
5.2 遵守里氏替換原則
取消幾維鳥原來的繼承關系,定義鳥和幾維鳥的更一般的父類,如動物類,它們都有奔跑的能力。幾維鳥的飛行速度雖然為 0,但奔跑速度不為 0,可以計算出其奔跑 300 千米所要花費的時間。
public class Lsptest2 {
public static void main(String[] args) {
Animal animal1 = new Bird();
Animal animal2 = new BrownKiwi();
animal1.setRunSpeed(120);
animal2.setRunSpeed(180);
System.out.println("如果奔跑300公里:");
try {
System.out.println("鳥類將奔跑" + animal1.getRunSpeed(300) + "小時.");
System.out.println("幾維鳥將奔跑" + animal2.getRunSpeed(300) + "小時。");
Bird bird = new Swallow();
bird.setFlySpeed(150);
System.out.println("如果飛行300公里:");
System.out.println("燕子將飛行" + bird.getFlyTime(300) + "小時.");
} catch (Exception err) {
System.out.println("發(fā)生錯誤了!");
}
}
}
/**
* 動物類,抽象的功能更加具有共性
*/
class Animal{
Double runSpeed;
public void setRunSpeed(double runSpeed) {
this.runSpeed = runSpeed;
}
public double getRunSpeed(double distince) {
return distince/runSpeed;
}
}
/**
* 鳥類繼承動物類
*/
class Bird extends Animal{
double flySpeed;
public void setFlySpeed(double flySpeed) {
this.flySpeed = flySpeed;
}
public double getFlyTime(double distince) {
return distince/flySpeed;
}
}
/**
* 幾維鳥繼承動物類
*/
class BrownKiwi extends Animal{
}
/**
* 燕子繼承鳥類 飛行屬于燕子的特性,
*/
class Swallow extends Bird{
}
06.里氏替換優(yōu)缺點
子類應該能夠替代父類并且表現出相同的行為,而不需要修改原有的程序邏輯。這樣可以確保代碼的可擴展性、可維護性和可重用性。
遵循里式替換原則的好處包括:
- 代碼的可擴展性:可以通過添加新的子類來擴展系統(tǒng)的功能,而不需要修改現有的代碼。
- 代碼的可維護性:當需要修改系統(tǒng)的行為時,只需要修改子類的代碼,而不需要修改其他相關的代碼。
- 代碼的可重用性:可以通過使用父類的對象來處理子類的對象,從而提高代碼的重用性。
它也有一些潛在的缺點和限制,包括:
- 過度設計:過度遵循LSP可能導致過度設計。為了確保子類能夠無縫替換父類,可能需要在子類中添加許多條件和限制,這可能會增加代碼的復雜性和維護成本。
- 難以滿足所有情況:在某些情況下,很難設計出滿足LSP的完美繼承關系。特定的業(yè)務需求和復雜性可能導致無法完全滿足LSP的要求,需要在設計中做出權衡和妥協。
- 可能引入不必要的復雜性:為了滿足LSP,可能需要引入額外的抽象層次和接口,這可能增加代碼的復雜性和理解難度。
07.里式替換原則總結
7.1 一些總結和分析
里氏替換原則與開閉原則的關系
里氏替換原則與開閉原則密切相關。開閉原則強調對擴展開放、對修改關閉,而里氏替換原則則確保子類能夠正確地替換父類,使得擴展在不修改現有代碼的情況下進行。
在電商交易系統(tǒng)中,遵循里氏替換原則可以確保我們在擴展支付方式、引入新的支付邏輯時,不會破壞已有系統(tǒng)的穩(wěn)定性。例如,我們可以添加新的支付方式,而不影響原有的支付邏輯。
里式替換原則是用來指導,繼承關系中子類該如何設計的一個原則
理解里式替換原則,最核心的就是理解“design by contract,按照協議來設計”這幾個字。
父類定義了函數的“約定”(或者叫協議),那子類可以改變函數的內部實現邏輯,但不能改變函數原有的“約定”。
這里的約定包括:函數聲明要實現的功能;對輸入、輸出、異常的約定;甚至包括注釋中所羅列的任何特殊說明。
要弄明白里式替換原則跟多態(tài)的區(qū)別
雖然從定義描述和代碼實現上來看,多態(tài)和里式替換有點類似,但它們關注的角度是不一樣的。
- 多態(tài)是面向對象編程的一大特性,也是面向對象編程語言的一種語法。它是一種代碼實現的思路。多態(tài)是指,子類可以替換父類,在實際的代碼運行過程中,調用子類的方法實現。
- 里式替換是一種設計原則,用來指導繼承關系中子類該如何設計,子類的設計要保證在替換父類的時候,不改變原有程序的邏輯及不破壞原有程序的正確性。
7.2 里式替換原則總結
- 里式替換問題思考:什么是里氏替換的原則?有那些場景滿足里氏替換原則?它跟多態(tài)有何區(qū)別?
- 如何理解里式替換原則:子類可以替換父類,并且保證原有的邏輯不變以及正確性不被破壞。
- 列舉一個里氏替換的場景:比如支付寶,微信支付。將通用支付校驗邏輯放到父類中,支付子類繼承父類進行支付,支付金額都符合父類的約束。
- 里式替換原則的背景:面向對象中繼承是一種機制,子類可以繼承父類屬性和行為。當使用繼承時,子類不會破壞父類原有程序功能,這就是里氏替換的背景。
- 實現里式替換原則的方式:應該確保子類在擴展父類功能時,保持父類的行為一致性。簡單說就是父類抽取通用的邏輯。
- 里式替換原則的案例教學:通用支付類中,有對金額進行校驗,微信和支付寶支付子類通過繼承父類,分別拓展自己的業(yè)務邏輯,且金額校驗邏輯受到父類的約束。
- 里式替換原則的優(yōu)點:子類替代父類行為,且不需要修改原有邏輯。可以確保代碼可拓展,可維護,可重用等優(yōu)點。
- 里式替換原則的缺點:存在過度設計,需要引入額外的抽象層次可能增加代碼復雜性,等等。
- 總結如何理解里式替換原則:1.有繼承關系;2.子類遵循父類約定;3.子類拓展行為不破壞父類行為。
- 里式替換原則跟多態(tài)的區(qū)別:多態(tài)是面向對象特性,是一個語法,子類可以替換父類實現。里氏替換是設計原則,是指子類設計要保證替換父類時,不改變原有邏輯和正確性!
7.3 更多內容推薦
模塊 | 描述 | 備注 |
---|---|---|
GitHub | 多個YC系列開源項目,包含Android組件庫,以及多個案例 | GitHub |
博客匯總 | 匯聚Java,Android,C/C++,網絡協議,算法,編程總結等 | YCBlogs |
設計模式 | 六大設計原則,23種設計模式,設計模式案例,面向對象思想 | 設計模式 |
Java進階 | 數據設計和原理,面向對象核心思想,IO,異常,線程和并發(fā),JVM | Java高級 |
網絡協議 | 網絡實際案例,網絡原理和分層,Https,網絡請求,故障排查 | 網絡協議 |
計算機原理 | 計算機組成結構,框架,存儲器,CPU設計,內存設計,指令編程原理,異常處理機制,IO操作和原理 | 計算機基礎 |
學習C編程 | C語言入門級別系統(tǒng)全面的學習教程,學習三到四個綜合案例 | C編程 |
C++編程 | C++語言入門級別系統(tǒng)全面的教學教程,并發(fā)編程,核心原理 | C++編程 |
算法實踐 | 專欄,數組,鏈表,棧,隊列,樹,哈希,遞歸,查找,排序等 | Leetcode |
Android | 基礎入門,開源庫解讀,性能優(yōu)化,Framework,方案設計 | Android |