Java設計模式(4:里氏替換原則和合成復用原則詳解
一、里氏替換原則
如果說實現開閉原則的關鍵步驟就是抽象化,那麼基類(父類)和子類的繼承關係就是抽象化的具體實現,所以里氏替換原則就是對實現抽象化的具體步驟的規範。即:子類可以擴展基類(父類)的功能,但不能改變父類原有的功能。
定義:一個軟件實體如果適用一個父類的話,那一定是適用於其子類,所有引用父類的地方必須能透明地使用其子類的對象,子類對象能夠替換父類對象,而程序邏輯不變。
里氏替換原則最核心得一句話就是:子類可以擴展基類(父類)的功能,但不能改變父類原有的功能。它包含着四種含義:
- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
- 子類可以增持自己特有的方法。
- 當子類的方法重載父類的方法時,方法的前置條件(即:方法的參數)要比父類方法的輸入參數更為寬鬆。
- 當子類的方法實現父類的方法時(重寫/重載/實現抽象方法),方法的後置條件(即:返回值)要比父類更為更為嚴格或者相等。
我們先來做一個簡單的計算器的功能,創建一個類SumA
,實現一個兩數相減的功能reduce()
:
public class SumA {
// 相減
public int reduce(int a,int b){
return a - b;
}
}
再來創建一個類SumB
,增加一個兩數相加的功能,並且SumB
是SumA
的子類:
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));
}
結果:
這麼看起來結果沒有錯,那麼根據里氏替換原則的定義:一個軟件實體如果適用一個父類的話,那一定是適用於其子類,所有引用父類的地方必須能透明地使用其子類的對象,子類對象能夠替換父類對象,而程序邏輯不變。
我們來將對象換成SumA
的子類SumB
的對象再來測試一下:
public static void main(String[] args) {
SumA sumA = new SumB();
System.out.println("5 - 4 = "+sumA.reduce(5,4));
}
結果:
可以看見結果發生了很大的變化,通過仔細查看代碼我們發現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));
}
當然也有人說,如果非要重寫父類的方法該怎麼辦?我這邊建議兩個方法:
- 將現有的繼承關係去掉,讓
SumA
和SumB
類都實現同一個接口Sum
類,然後再重寫Sum
類中的reduce()
方法。 - 讓
SumA
和SumB
都繼承一個比較通俗的基類(父類),將現有的繼承關係去掉,採用依賴、聚合,組合等關係代替。
二、合成復用原則
盡量使用對象組合/聚合,而不是使用繼承達到軟件復用的目的。可以使系統更加的靈活,降低類與類之間的耦合度,一個類的變化對於其他類來說影響相對較少。
繼承我們稱之為白箱復用,相當於把實現的細節暴露給子類,組合/聚合 也成為黑箱復用,對類之外的對象是無法獲取到實現細節的。
合成復用原則的核心是:復用時要盡量使用組合/聚合關係(關聯關係),少用繼承。
我們先來看一個數據庫連接的例子:
// 數據庫連接
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
類的子類的對象就可以了。
類圖:
最後
設計模式中的七大原則已經講完了,共有四篇博客,感興趣的朋友可以去我的博客空間看看。
從下一篇博客開始,我將開始講解一下Java
中常見的以及我們經常用到的一些設計模式,包括工廠模式、代理模式、單例……如果有興趣的朋友可以繼續關注我,讓我們一同進步,謝謝!