Java設計模式(4:里氏替換原則和合成復用原則詳解

一、里氏替換原則

如果說實現開閉原則的關鍵步驟就是抽象化,那麼基類(父類)和子類的繼承關係就是抽象化的具體實現,所以里氏替換原則就是對實現抽象化的具體步驟的規範。即:子類可以擴展基類(父類)的功能,但不能改變父類原有的功能。

定義:一個軟件實體如果適用一個父類的話,那一定是適用於其子類,所有引用父類的地方必須能透明地使用其子類的對象,子類對象能夠替換父類對象,而程序邏輯不變。

里氏替換原則最核心得一句話就是:子類可以擴展基類(父類)的功能,但不能改變父類原有的功能。它包含着四種含義:

  1. 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
  2. 子類可以增持自己特有的方法。
  3. 當子類的方法重載父類的方法時,方法的前置條件(即:方法的參數)要比父類方法的輸入參數更為寬鬆。
  4. 當子類的方法實現父類的方法時(重寫/重載/實現抽象方法),方法的後置條件(即:返回值)要比父類更為更為嚴格或者相等。

我們先來做一個簡單的計算器的功能,創建一個類SumA,實現一個兩數相減的功能reduce()

public class SumA {
    // 相減
    public int reduce(int a,int b){
        return a - b;
    }
}

再來創建一個類SumB,增加一個兩數相加的功能,並且SumBSumA的子類:

public class SumB extends SumA {
    // 相加
    public int reduce(int a,int b){
        return a + b;
    }
}

測試一下:

public static void main(String[] args) {
    SumB sumB = new SumB();
    System.out.println("5 - 4 = "+sumB.reduce(5,4));
}

結果:

image20210610094247295.png

這麼看起來結果沒有錯,那麼根據里氏替換原則的定義:一個軟件實體如果適用一個父類的話,那一定是適用於其子類,所有引用父類的地方必須能透明地使用其子類的對象,子類對象能夠替換父類對象,而程序邏輯不變

我們來將對象換成SumA的子類SumB的對象再來測試一下:

public static void main(String[] args) {
    SumA sumA = new SumB();
    System.out.println("5 - 4 = "+sumA.reduce(5,4));
}

結果:

image20210610094634682.png

可以看見結果發生了很大的變化,通過仔細查看代碼我們發現SumA的兩數相減方法reduce()SumB的兩數相加方法reduce()名字相同。這麼來就可以說SumB重寫了SumA中的非抽象方法reduce(),並改變了reduce()方法的行為,使程序發生了很大的漏洞。所以我們來將SumB類進行改造:

public class SumB extends SumA {
    // 相加
    public int add(int a,int b){
        return a + b;
    }
}

SumB類中增加一個add()方法,這樣一來SumB作為子類,既可以調用自己類中的add()方法,也可以調用父類SumA中的reduce()方法。我們再來測試一下:

public static void main(String[] args) {
    SumB sumB = new SumB();
    System.out.println("5 - 4 = "+sumB.reduce(5,4));
    System.out.println("5 + 4 = "+sumB.add(5,4));
}

image20210610095807092.png

當然也有人說,如果非要重寫父類的方法該怎麼辦?我這邊建議兩個方法:

  1. 將現有的繼承關係去掉,讓SumASumB類都實現同一個接口Sum類,然後再重寫Sum類中的reduce()方法。
  2. SumASumB都繼承一個比較通俗的基類(父類),將現有的繼承關係去掉,採用依賴、聚合,組合等關係代替。

二、合成復用原則

盡量使用對象組合/聚合,而不是使用繼承達到軟件復用的目的。可以使系統更加的靈活,降低類與類之間的耦合度,一個類的變化對於其他類來說影響相對較少。

繼承我們稱之為白箱復用,相當於把實現的細節暴露給子類,組合/聚合 也成為黑箱復用,對類之外的對象是無法獲取到實現細節的。

合成復用原則的核心是:復用時要盡量使用組合/聚合關係(關聯關係),少用繼承

我們先來看一個數據庫連接的例子:

// 數據庫連接
public class DBConnection {

    //MySQL數據連接
    public String getConnection(){
        return "MySQL數據庫連接......";
    }
}
// 產品類 dao
public class ProductDAO {

    private DBConnection dbConnection;

    public void setDbConnection(DBConnection dbConnection) {
        this.dbConnection = dbConnection;
    }

    public void addProduct(){
        String connection = dbConnection.getConnection();
        System.out.println("使用【"+connection+"】增加產品");
    }
}

DBConnection是一個提供數據庫連接的類,目前只支持MySQL數據庫連接的方法。某一天,客戶要求增加一個Oracle數據庫連接的產品,那我們先在DBConnection增加一個getOracleConnection()的方法,再去修改ProductDAO類中的代碼?這裡且不說已經違反了開閉原則,就是各種代碼的複製粘貼也讓人心煩的,完全不夠簡潔、優雅。

我們不用去修改ProductDAO類中的代碼,只需要將DBConnection類的代碼改動一下:

// 數據庫連接
public abstract class DBConnection {

    //數據庫連接方法
    public abstract String getConnection();
}

如上面的代碼,將DBConnection類改為抽象類,將getConnection()方法改為抽象方法。這樣一來,如果我們需要MySQL數據庫連接,就增加一個MySQLConnection類來繼承DBConnection類:

public class MySQLConnection extends DBConnection {

    @Override
    public String getConnection() {
        return "MySQL數據庫連接......";
    }
}

如果我們需要Oracle數據庫連接,就增加一個OracleConnection類來繼承DBConnection類:

public class OracleConnection extends DBConnection {

    @Override
    public String getConnection() {
        return "Oracle數據庫連接......";
    }
}

最後在調用ProductDAO類中的addProduct()方法前,我們只需要調用setDbConnection()方法並傳入我們所需要的DBConnection類的子類的對象就可以了。

類圖:

image20210610104159716.png

最後


設計模式中的七大原則已經講完了,共有四篇博客,感興趣的朋友可以去我的博客空間看看。

從下一篇博客開始,我將開始講解一下Java中常見的以及我們經常用到的一些設計模式,包括工廠模式、代理模式、單例……如果有興趣的朋友可以繼續關注我,讓我們一同進步,謝謝!