寂然解讀設計模式 - 里氏替換原則

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é),最后,希望大家在學習的過程中,能夠感覺到設計模式的有趣之處,高效而愉快的學習,那我們下期見~

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

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