I walk very slowly, but I never walk backwards
設計模式原則 - 里氏替換原則
? 寂然
大家好,我是寂然~,本節(jié)課呢,我來給大家介紹設計模式原則之里氏替換原則,話不多說,我們直接進入正題,老規(guī)矩,首先帶大家了解一下里氏替換原則的官方定義,并作一個解釋,但是在此之前,我們先來聊聊ava面向對象最重要的特性之一 - 繼承性
前情提要 - 聊聊繼承性
繼承性相信大家已經(jīng)十分熟悉了,繼承是面向對象的很重要的特性之一,其實我們今天課程要講的里氏替換原則,就是要告訴我們,在編程中,如何正確的使用繼承,這里有伙伴要問了,正確的使用怎么解?OK,那我們先來聊聊,分析下繼承的優(yōu)勢和劣勢
繼承優(yōu)勢
● 提高代碼的復用性( 每個子類都擁有父類的方法和屬性 )
● 提高代碼的可擴展性( 很多開源框架的擴展接口都是通過繼承父類來完成的 )
繼承劣勢
● 繼承是侵入性的( 只要繼承,就必須擁有父類的所有屬性和方法)
● 繼承機制很大的增加了耦合性( 如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,并且父類修改后,所有涉及到子類的功能都可能產(chǎn)生故障)
上面提到了,里氏替換原則,就是要告訴我們,在編程中,如何正確的使用繼承,帶著這樣的疑問,我們 先來看下里氏替換原則的官方定義
官方定義
里氏替換原則(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的行為沒有發(fā)生變化,那么類型S是類型T的子類型
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基類的地方必須能透明地使用其子類的對象
基本介紹
里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能
其實繼承中包含這樣一層含義:父類中凡是已經(jīng)實現(xiàn)好的方法,實際上是在設定規(guī)范和契約,雖然繼承不強制要求,所有的子類必須遵守這些契約,但是如果子類對這些已經(jīng)實現(xiàn)的方法任意修改,就會對整個繼承體系造成破壞
上面我們提到,繼承給程序設計帶來便利的同時,也帶來了弊端,里氏替換原則即是給繼承性制定了規(guī)范
案例演示 - 計算器
為了讓大家體會一下我們上面說的,我們通過一個案例來詳細說明一下
假設現(xiàn)在有一個計算器類,可以進行加法減法計算,我們定義其子類,進行需求的增補,簡易代碼如下:
//定義計算器類
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{
//增補需求(兩數(shù)相加之和 +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,我們對上述代碼進行簡單的測試,可以看到,子類需要實現(xiàn)需求,無意間重寫了父類的方法
public static void main(String[] args) {
int mulResult = new HjCalculayor().mul(2, 3);
System.out.println("二者相加之和再與100相減的結果為" + mulResult);
//運行結果:二者相加之和再與100相減的結果為90 出現(xiàn)問題
}
案例分析
我們發(fā)現(xiàn)原來運行正常的mul()方法發(fā)生了錯誤,原因就是子類 HjCalculayor 無意中重寫了父類的方法,造成原有功能出現(xiàn)錯誤,在實際編程中,我們常常會通過重寫父類的方法完成新的功能,這樣寫起來雖然簡單,但整個繼承體系的復用性會比較差,特別是運行多態(tài)比較頻繁的時候 ,針對上述問題,我們來聊聊解決方案
解決方案
上面出現(xiàn)的情況,其實就是里氏替換原則擔心的,我們可以擴展,但是不能改變父類原有的功能,里氏替換原則雖然這樣說,但并非讓我們因噎廢食,放棄使用繼承,我們可以通過其它方式來解決繼承所帶來的弊端,如:組合、聚合、依賴等方式,當然,這些后面在類關系中都會給大家展開深入講解
比如這里,其中一種解決方案是讓原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,如果類HjCalculayor 需要使用類 Calculator的方法,將二者變?yōu)榻M合關系來完成需求
//創(chuàng)建一個更加基礎的基類
//把更加基礎,需要復用的成員/方法寫到基類中
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();
//增補需求(兩數(shù)相加之和 +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;
}
}
這樣可以看到,在完成業(yè)務邏輯時,明確調(diào)用 calculator.add() 方法,這樣既符合里氏替換原則,子類避免改變父類原有的功能,同時定義一個更加通俗的基類,改變原有的繼承關系,也可以保證整個繼承體系的復用性
深度解析
里氏替換原則其實還有以下兩個含義,我們一起來聊聊
一、子類可以實現(xiàn)父類的抽象方法,但是不能覆蓋父類的非抽象方法
在我們做系統(tǒng)設計時,經(jīng)常會設計接口或抽象類,然后由子類來實現(xiàn)抽象方法,這里使用的其實也是里氏替換原則,子類可以實現(xiàn)父類的抽象方法很好理解,事實上,子類也必須完全實現(xiàn)父類的抽象方法,哪怕寫一個空方法,否則會編譯報錯,里氏替換原則的關鍵點在于不能覆蓋父類的非抽象方法,這是他著重強調(diào)的
二、子類中可以增加自己特有的方法
在繼承父類屬性和方法的同時,每個子類也都可以有自己的個性,在父類的基礎上擴展自己的功能,前面其實已經(jīng)提到,當功能擴展時,子類不要重寫父類的方法,而是另寫一個方法
注意事項
第一就是我們上面提到的,里氏替換原則雖然指出了繼承帶來的一些弊端,但是并非讓我們放棄使用繼承,而是給我們制定了編程中正確使用繼承的規(guī)范,這是需要和大家再次強調(diào)的
第二,里氏代換原則是實現(xiàn)開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,因此在程序中盡量使用基類類型來對對象進行定義,而在運行時再確定其子類類型,用子類對象來替換父類對象
下節(jié)預告
OK,那既然上面提到了,里氏代換原則是實現(xiàn)開閉原則的重要方式之一,那我們掌握了里氏替換原則,下一節(jié),我們正式進入開閉原則的學習,我會為大家用多個案例分析,來解讀設計模式原則之開閉原則,以及它的注意事項和細節(jié),最后,希望大家在學習的過程中,能夠感覺到設計模式的有趣之處,高效而愉快的學習,那我們下期見~