一、SOLID
設計模式的六大原則有:
- Single Responsibility Principle:單一職責原則
- Open Closed Principle:開閉原則
- Liskov Substitution Principle:里氏替換原則
- Law of Demeter:迪米特法則
- Interface Segregation Principle:接口隔離原則
- Dependence Inversion Principle:依賴倒置原則
把這六個原則的首字母聯合起來(兩個 L 算做一個)就是 SOLID (solid,穩定的),其代表的含義就是這六個原則結合使用的好處:建立穩定、靈活、健壯的設計。下面我們來看一下里氏替換原則。
設計模式六大原則(SOLID)
二、里氏替換原則定義
【所有引用基類的地方必須能透明地使用其子類的對象】
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
三、里氏替換原則彌補繼承的缺陷
氏替換原則的意思是,所有基類在的地方,都可以換成子類,程序還可以正常運行。這個原則是與面向對象語言的繼承特性密切相關的。
在學習java類的繼承時,我們知道繼承有一些優點:
- 子類擁有父類的所有方法和屬性,從而可以減少創建類的工作量。
- 提高了代碼的重用性。
- 提高了代碼的擴展性,子類不但擁有了父類的所有功能,還可以添加自己的功能。
但有優點也同樣存在缺點:
- 繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法。
- 降低了代碼的靈活性。因為繼承時,父類會對子類有一種約束。
- 增強了耦合性。當需要對父類的代碼進行修改時,必須考慮到對子類產生的影響。
如何揚長避短呢?方法是引入里氏替換原則。
四、里氏替換原則對繼承進行了規則上的約束
里氏替換原則對繼承進行了規則上的約束,這種約束主要體現在四個方面:
-
子類必須實現父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現)方法。
-
子類中可以增加自己特有的方法。
-
當子類覆蓋或實現父類的方法時,方法的前置條件(即方法的形參)要比- 父類方法的輸入參數更寬松。(即只能重載不能重寫)
-
當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
下面對以上四個含義進行詳細的闡述
4.1、子類必須實現父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現)方法
在我們做系統設計時,經常會設計接口或抽象類,然后由子類來實現抽象方法,這里使用的其實就是里氏替換原則。若子類不完全對父類的方法進行實例化,那么子類就不能被實例化,那么這個接口或抽象類就毫無存在的意義了。
里氏替換原則規定,子類不能覆寫父類已實現的方法。父類中已實現的方法其實是一種已定好的規范和契約,如果我們隨意的修改了它,那么可能會帶來意想不到的錯誤。下面舉例說明一下子類覆寫了父類方法帶來的后果。
public class Father { public void fun(int a, int b) { System.out.println(a + "+" + b + "=" + (a + b)); } } public class Sun extends Father { @Override public void fun(int a, int b) { System.out.println(a + "-" + b + "=" + (a - b)); } } public class Client { public static void main(String[] args) { Father father = new Father(); father.fun(1, 2); // 父類存在的地方,可以用子類替代 // 子類Sun替代父類Father System.out.println("子類替代父類后的運行結果"); Sun sun = new Sun(); sun.fun(1, 2); } }
運行結果:
1+2=3
子類替代父類后的運行結果
1-2=-1
我們想要的結果是“1+2=3”。可以看到,方法重寫后結果就不是了我們想要的結果了,也就是這個程序中子類B不能替代父類A。這違反了里氏替換原則,從而給程序造成了錯誤。
我們可以給父類的非抽象(已實現)方法加final修飾,這樣就在語法層面控制了父類非抽象方法被子類重寫而違反里氏替換原則。
有時候父類有多個子類(Sun1、Sun2),但在這些子類中有一個特例(Sun2)。要想滿足里氏替換原則,又想滿足這個子類的功能時,有的伙伴可能會修改父類(Father)的方法。但是,修改了父類的方法又會對其他的子類造成影響,產生更多的錯誤。這是怎么辦呢?我們可以為這個特例(Sun2)創建一個新的父類(Father2),這個新的父類擁有原父類的部分功能(Father2並不繼承Father,而是持有Father的一個引用,組合Father,調用Father里的功能),又有不同的功能。這樣既滿足了里氏替換原則,又滿足了這個特例的需求。
4.2、子類中可以增加自己特有的方法
這個很容易理解,子類繼承了父類,擁有了父類和方法,同時還可以定義自己有,而父類沒有的方法。這是在繼承父類方法的基礎上進行功能的擴展,符合里氏替換原則。
4.3 、當子類覆蓋或實現父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松
先看一段代碼:
public class Father { public void fun(HashMap map){ System.out.println("父類被執行..."); } } public class Sun extends Father { public void fun(Map map){ System.out.println("子類被執行..."); } } public class Client { public static void main(String[] args) { System.out.print("父類的運行結果:"); Father father=new Father(); HashMap map=new HashMap(); father.fun(map); //父類存在的地方,可以用子類替代 //子類B替代父類A System.out.print("子類替代父類后的運行結果:"); Sun sun=new Sun(); sun.fun(map); } }
運行結果:
父類的運行結果:父類被執行...
子類替代父類后的運行結果:父類被執行...
我們應當主意,子類並非重寫了父類的方法,而是重載了父類的方法。因為子類和父類的方法的輸入參數是不同的。子類方法的參數Map比父類方法的參數HashMap的范圍要大,所以當參數輸入為HashMap類型時,只會執行父類的方法,不會執行子類的重載方法。這符合里氏替換原則。
但如果我將子類方法的參數范圍縮小會怎樣?看代碼:
public class Father { public void fun(Map map){ System.out.println("父類被執行..."); } } public class Sun extends Father { public void fun(HashMap map){ System.out.println("子類被執行..."); } } public class Client { public static void main(String[] args) { System.out.print("父類的運行結果:"); Father father=new Father(); HashMap map=new HashMap(); father.fun(map); //父類存在的地方,可以用子類替代 //子類B替代父類A System.out.print("子類替代父類后的運行結果:"); Sun sun=new Sun(); sun.fun(map); } }
運行結果:
父類的運行結果:父類被執行...
子類替代父類后的運行結果:子類被執行...
在父類方法沒有被重寫的情況下,子方法被執行了,這樣就引起了程序邏輯的混亂。所以子類中方法的前置條件必須與父類中被覆寫的方法的前置條件相同或者更寬松。
4.4、當子類的方法實現父類的(抽象)方法時,方法的后置條件(即方法的返回值)要比父類更嚴格
public abstract class Father { public abstract Map fun(); } public class Sun extends Father { @Override public HashMap fun() { System.out.println("子類被執行..."); return null; } } public class Client { public static void main(String[] args) { Father father=new Sun(); father.fun(); } }
運行結果:
子類被執行...
注意:是實現父類的抽象方法,而不是父類的非抽象(已實現)方法,不然就違法了第一條。
若在繼承時,子類的方法返回值類型范圍比父類的方法返回值類型范圍大,在子類重寫該方法時編譯器會報錯。(Java語法)