I walk very slowly, but I never walk backwards
設計模式原則 - 裡氏替換原則
寂然
大家好,我是寂然~,本節課呢,我來給大家介紹設計模式原則之裡氏替換原則,話不多說,我們直接進入正題,老規矩,首先帶大家了解一下裡氏替換原則的官方定義,并作一個解釋,但是在此之前,我們先來聊聊ava面向對象最重要的特性之一 - 繼承性
前情提要 - 聊聊繼承性
繼承性相信大家已經十分熟悉了,繼承是面向對象的很重要的特性之一,其實我們今天課程要講的裡氏替換原則,就是要告訴我們,在程式設計中,如何正确的使用繼承,這裡有夥伴要問了,正确的使用怎麼解?OK,那我們先來聊聊,分析下繼承的優勢和劣勢
繼承優勢
● 提高代碼的複用性( 每個子類都擁有父類的方法和屬性 )
● 提高代碼的可擴充性( 很多開源架構的擴充接口都是通過繼承父類來完成的 )
繼承劣勢
● 繼承是侵入性的( 隻要繼承,就必須擁有父類的所有屬性和方法)
● 繼承機制很大的增加了耦合性( 如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,并且父類修改後,所有涉及到子類的功能都可能産生故障)
上面提到了,裡氏替換原則,就是要告訴我們,在程式設計中,如何正确的使用繼承,帶着這樣的疑問,我們 先來看下裡氏替換原則的官方定義
官方定義
裡氏替換原則(Liskov Substitution Principle,LSP)是1988年,麻省理工學院一位姓裡的女士提出的,官方定義如下:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程式P在所有的對象o1都代換成o2時,程式P的行為沒有發生變化,那麼類型S是類型T的子類型
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基類的地方必須能透明地使用其子類的對象
基本介紹
裡氏替換原則通俗的來講就是:子類可以擴充父類的功能,但不能改變父類原有的功能
其實繼承中包含這樣一層含義:父類中凡是已經實作好的方法,實際上是在設定規範和契約,雖然繼承不強制要求,所有的子類必須遵守這些契約,但是如果子類對這些已經實作的方法任意修改,就會對整個繼承體系造成破壞
上面我們提到,繼承給程式設計帶來便利的同時,也帶來了弊端,裡氏替換原則即是給繼承性制定了規範
案例示範 - 電腦
為了讓大家體會一下我們上面說的,我們通過一個案例來詳細說明一下
假設現在有一個電腦類,可以進行加法減法計算,我們定義其子類,進行需求的增補,簡易代碼如下:
//定義電腦類
class Calculator{
//定義加法計算
public int add(int a,int b){
int result = a + b;
return result;
}
//定義減法計算
public int sub(int a,int b){
int result = a - b;
return result;
}
}
//定義其子類
class HjCalculayor extends Calculator{
//增補需求(兩數相加之和 +5) 無意中重寫了父類的方法
public int add(int a,int b){
int result = a + b + 5;
return result;
}
//需求:二者相加之和,與100相減
public int mul(int a,int b){
int count = add(a, b);
int result = 100 - count;
return result;
}
}
OK,我們對上述代碼進行簡單的測試,可以看到,子類需要實作需求,無意間重寫了父類的方法
public static void main(String[] args) {
int mulResult = new HjCalculayor().mul(2, 3);
System.out.println("二者相加之和再與100相減的結果為" + mulResult);
//運作結果:二者相加之和再與100相減的結果為90 出現問題
}
案例分析
我們發現原來運作正常的mul()方法發生了錯誤,原因就是子類 HjCalculayor 無意中重寫了父類的方法,造成原有功能出現錯誤,在實際程式設計中,我們常常會通過重寫父類的方法完成新的功能,這樣寫起來雖然簡單,但整個繼承體系的複用性會比較差,特别是運作多态比較頻繁的時候 ,針對上述問題,我們來聊聊解決方案
解決方案
上面出現的情況,其實就是裡氏替換原則擔心的,我們可以擴充,但是不能改變父類原有的功能,裡氏替換原則雖然這樣說,但并非讓我們因噎廢食,放棄使用繼承,我們可以通過其它方式來解決繼承所帶來的弊端,如:組合、聚合、依賴等方式,當然,這些後面在類關系中都會給大家展開深入講解
比如這裡,其中一種解決方案是讓原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,如果類HjCalculayor 需要使用類 Calculator的方法,将二者變為組合關系來完成需求
//建立一個更加基礎的基類
//把更加基礎,需要複用的成員/方法寫到基類中
class Base{
//TODO...
}
//定義電腦類
class Calculator extends Base{
//定義加法計算
public int add(int a,int b){
int result = a + b;
return result;
}
//定義減法計算
public int sub(int a,int b){
int result = a - b;
return result;
}
}
class HjCalculayor extends Base{
//如果 HjCalculayor需要使用 Calculator 類的方法,使用組合關系
private Calculator calculator = new Calculator();
//增補需求(兩數相加之和 +5)
public int add(int a,int b){
int result = a + b + 5;
return result;
}
//需求:二者相加之和,與100相減
public int mul(int a,int b){
int count = calculator.add(a, b);
int result = 100 - count;
return result;
}
}
這樣可以看到,在完成業務邏輯時,明确調用 calculator.add() 方法,這樣既符合裡氏替換原則,子類避免改變父類原有的功能,同時定義一個更加通俗的基類,改變原有的繼承關系,也可以保證整個繼承體系的複用性
深度解析
裡氏替換原則其實還有以下兩個含義,我們一起來聊聊
一、子類可以實作父類的抽象方法,但是不能覆寫父類的非抽象方法
在我們做系統設計時,經常會設計接口或抽象類,然後由子類來實作抽象方法,這裡使用的其實也是裡氏替換原則,子類可以實作父類的抽象方法很好了解,事實上,子類也必須完全實作父類的抽象方法,哪怕寫一個空方法,否則會編譯報錯,裡氏替換原則的關鍵點在于不能覆寫父類的非抽象方法,這是他着重強調的
二、子類中可以增加自己特有的方法
在繼承父類屬性和方法的同時,每個子類也都可以有自己的個性,在父類的基礎上擴充自己的功能,前面其實已經提到,當功能擴充時,子類不要重寫父類的方法,而是另寫一個方法
注意事項
第一就是我們上面提到的,裡氏替換原則雖然指出了繼承帶來的一些弊端,但是并非讓我們放棄使用繼承,而是給我們制定了程式設計中正确使用繼承的規範,這是需要和大家再次強調的
第二,裡氏代換原則是實作開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,是以在程式中盡量使用基類類型來對對象進行定義,而在運作時再确定其子類類型,用子類對象來替換父類對象
下節預告
OK,那既然上面提到了,裡氏代換原則是實作開閉原則的重要方式之一,那我們掌握了裡氏替換原則,下一節,我們正式進入開閉原則的學習,我會為大家用多個案例分析,來解讀設計模式原則之開閉原則,以及它的注意事項和細節,最後,希望大家在學習的過程中,能夠感覺到設計模式的有趣之處,高效而愉快的學習,那我們下期見~