1.概念
里氏代換原則(Liskov Substitution Principle LSP)面向對象設計的基本原則之一。 里氏代換原則中說,任何基類可以出現的地方,子類一定可以出現。 LSP是繼承復用的基石,只有當衍生類可以替換掉基類,軟件單位的功能不受到影響時,基類才能真正被復用,而衍生類也能夠在基類的基礎上增加新的行為。里氏代換原則是對“開-閉”原則的補充。實現“開-閉”原則的關鍵步驟就是抽象化。而基類與子類的繼承關系就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規范。
通俗的講:
1.所有引用基類的地方必須能透明的使用其子類的對象。其父類可以替換成子類,而子類不能替換成父類;
2.子類可以擴展父類的功能,但不能改變父類原有的功能;
2.舉例
例如:鳥一般都會飛行,如燕子的飛行速度大概是每小時 120 千米。但是新西蘭的幾維鳥由于翅膀退化無法飛行。假如要設計一個實例,計算這兩種鳥飛行 300 千米要花費的時間。顯然,拿燕子來測試這段代碼,結果正確,能計算出所需要的時間;但拿幾維鳥來測試,結果會發生“除零異常”或是“無窮大”,明顯不符合預期,類圖如下:
未遵守里氏替換原則:
package com.example.demo.principle;public class LSPtest {public static void main(String[] args) {Bird bird1 = new Swallow();Bird bird2 = new BrownKiwi();bird1.setSpeed(120);bird2.setSpeed(120);System.out.println("如果飛行300公里:");try {System.out.println("燕子將飛行" + bird1.getFlyTime(300) + "小時.");System.out.println("幾維鳥將飛行" + bird2.getFlyTime(300) + "小時。");} catch (Exception err) {System.out.println("發生錯誤了!");}}
}//鳥類
class Bird {double flySpeed;public void setSpeed(double speed) {flySpeed = speed;}public double getFlyTime(double distance) {return (distance / flySpeed);}
}//燕子類
class Swallow extends Bird {
}//幾維鳥類
class BrownKiwi extends Bird {public void setSpeed(double speed) {flySpeed = 0;}}------------------ 運行結果 --------------------------如果飛行300公里:
燕子將飛行2.5小時.
幾維鳥將飛行Infinity小時。Process finished with exit code 0
這個設計存在的問題:
-
幾維鳥類重寫了鳥類的 setSpeed(double speed) 方法,這違背了里氏替換原則。
-
燕子和幾維鳥都是鳥類,但是父類抽取的共性有問題,幾維鳥的的飛行不是正常鳥類的功能,需要特殊處理,應該抽取更加共性的功能。
遵守里氏替換原則
優化:
取消幾維鳥原來的繼承關系,定義鳥和幾維鳥的更一般的父類,如動物類,它們都有奔跑的能力。幾維鳥的飛行速度雖然為 0,但奔跑速度不為 0,可以計算出其奔跑 300 千米所要花費的時間。
package com.example.demo.principle;public class Lsptest2 {public static void main(String[] args) {Animal animal1 = new Bird();Animal animal2 = new BrownKiwi();animal1.setRunSpeed(120);animal2.setRunSpeed(180);System.out.println("如果奔跑300公里:");try {System.out.println("鳥類將奔跑" + animal1.getRunSpeed(300) + "小時.");System.out.println("幾維鳥將奔跑" + animal2.getRunSpeed(300) + "小時。");Bird bird = new Swallow();bird.setFlySpeed(150);System.out.println("如果飛行300公里:");System.out.println("燕子將飛行" + bird.getFlyTime(300) + "小時.");} catch (Exception err) {System.out.println("發生錯誤了!");}}
}/*** 動物類,抽象的功能更加具有共性*/class Animal{Double runSpeed;public void setRunSpeed(double runSpeed) {this.runSpeed = runSpeed;}public double getRunSpeed(double distince) {return distince/runSpeed;}}/*** 鳥類繼承動物類*/class Bird extends Animal{double flySpeed;public void setFlySpeed(double flySpeed) {this.flySpeed = flySpeed;}public double getFlyTime(double distince) {return distince/flySpeed;}}/*** 幾維鳥繼承動物類*/class BrownKiwi extends Animal{}/*** 燕子繼承鳥類 飛行屬于燕子的特性,*/class Swallow extends Bird{}--------- 運行結果 -----------------
如果奔跑300公里:
鳥類將奔跑2.5小時.
幾維鳥將奔跑1.6666666666666667小時。
如果飛行300公里:
燕子將飛行2.0小時.
3.優點
-
代碼共享,減少創建類的工作量,每個子類都擁有父類的方法和屬性;
-
提高代碼的重用性;
-
提高代碼的可擴展性;
-
提高產品或項目的開放性;
4.缺點
-
繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法;
-
降低代碼的靈活性。子類必須擁有父類的屬性和方法,讓子類自由的世界中多了些約束;
-
增強了耦合性。當父類的常量、變量和方法被修改時,需要考慮子類的修改,而且在缺乏規范的環境下,這種修改可能帶來非常糟糕的結果————大段的代碼需要重構。