一、里氏替換原則
如果說實現開閉原則的關鍵步驟就是抽象化,那么基類(父類)和子類的繼承關系就是抽象化的具體實現,所以里氏替換原則就是對實現抽象化的具體步驟的規范。即:子類可以擴展基類(父類)的功能,但不能改變父類原有的功能。
定義:一個軟件實體如果適用一個父類的話,那一定是適用於其子類,所有引用父類的地方必須能透明地使用其子類的對象,子類對象能夠替換父類對象,而程序邏輯不變。
里氏替換原則最核心得一句話就是:子類可以擴展基類(父類)的功能,但不能改變父類原有的功能。它包含着四種含義:
- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
- 子類可以增持自己特有的方法。
- 當子類的方法重載父類的方法時,方法的前置條件(即:方法的參數)要比父類方法的輸入參數更為寬松。
- 當子類的方法實現父類的方法時(重寫/重載/實現抽象方法),方法的后置條件(即:返回值)要比父類更為更為嚴格或者相等。
我們先來做一個簡單的計算器的功能,創建一個類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) {
SumA sumA = new SumA();
System.out.println("5 - 4 = "+sumA.reduce(5,4));
}
結果:

這么看起來結果沒有錯,那么根據里氏替換原則的定義:一個軟件實體如果適用一個父類的話,那一定是適用於其子類,所有引用父類的地方必須能透明地使用其子類的對象,子類對象能夠替換父類對象,而程序邏輯不變。
我們來將對象換成SumA的子類SumB的對象再來測試一下:
public static void main(String[] args) {
SumB sumB = new SumB();
System.out.println("5 - 4 = "+sumB.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中常見的以及我們經常用到的一些設計模式,包括工廠模式、代理模式、單例......如果有興趣的朋友可以繼續關注我,讓我們一同進步,謝謝!
