设计模式:里氏替换原则
里氏替換原則(Liskov Substitution Principle ,LSP):
指的是任何基類可以出現的地方,子類一定可以出現。
定義1
如果對每一個類型為T1的對象o1,都有類型為T2的對象o2,使得以T1定義的所有程序P在所有的對象o1都替換成o2時,程序p的行為沒有發生變化,那么類型T2是類型T1的子類型。
定義2
所有引用基類的地方必須能透明地使用其子類對象。
問題由來
有一功能P1,由類A完成。現需要將功能P1進行擴展,擴展后的功能為P,其中P由原功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。
解決方案
當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。
里氏替換原則包含了四層含義
1.子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。
實踐,以槍為例,看一下類圖
槍支類圖
槍支的抽象類:
public abstract class AbstractGun {public abstract void shoot(); }手槍,步槍實現類:
public class HandGun extends AbstractGun {public void shoot() {System.out.println("手機射擊"); } } public class Rifle extends AbstractGun {public void shoot() {System.out.println("步槍射擊"); } }士兵實現類:
public class Soldier {private AbstractGun gun;public void setGun(AbstractGun gun) {this.gun = gun;}public void killEnemy() {System.out.println("士兵殺敵人");gun.shoot();} }場景類:
public class Client {public static void main(String[] args) {Soldier sanMao = new Soldier();sanMao.setGun(new Rifle());sanMao.killEnemy();} }注意
在類中調用其他類時務必要使用父類或者接口(例如Solider類的setGun(AbstractGun gun)方法),否則說明類的設計已經違背了LSP原則。
現在有個玩具槍該怎么定義?直接繼承AbstractGun類嗎?如下:
public class ToyGun extends AbstractGun {@Overridepublic void shoot() {//玩具槍不能像真槍殺敵,不實現} }現在的場景類:
public class Client {public static void main(String[] args) {Soldier sanMao = new Soldier();sanMao.setGun(new ToyGun());sanMao.killEnemy();} }在這種情況下,士兵拿著玩具槍殺敵,發現業務調用類已經出現了問題,正常的業務邏輯運行結果是不正確的。(因為玩具槍并不能殺敵)ToyGun應該脫離繼承,建立一個獨立的類,可以與AbstractGun建立關聯委托關系。類圖如下:
玩具槍與真實槍分離
按照繼承的原則,ToyGun繼承AbstractGun是沒有問題的,但是在具體應用場景中就需考慮:子類是否能夠完整地實現父類的業務,否則就會出現上面的情況拿玩具槍殺敵。
注意
如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中發生重寫或者重載,則建議斷開父子繼承關系,采用依賴、聚集、組合等關系代替繼承。
2 子類中可以增加自己特性
子類當然可有自己的方法和屬性。里氏替換原則可以正著用,但是不能反著用,在子類出現的地方,父類未必可以勝任。
再說下面兩層含義之前先要明白 重載 重寫(覆蓋) 的區別:
重寫(覆蓋)的規則:
重載的規則:
3 類的方法重載父類的方法時,方法的前置條件(形參)要比父類方法的輸入參數更寬松.
實例:
public class Father {public Collection doSomething(HashMap map){System.out.println("父類被執行了");return map.values();} } public class Son extends Father{public Collection doSomething(Map map){System.out.println("子類被執行了");return map.values();} } public class Client{public static void main(String[] args) {invoker();}public static void invoker(){Son son = new Son();//子類對象HashMap map=new HashMap<>();son.doSomething(map);} }運行是”父類被執行了”,這是正確的,父類方法的參數是HashMap類型,而子類的方法參數是Map類型,子類的參數類型范圍比父類大,那么子類的方法永遠也不會執行。
4 覆寫或者實現父類的方法時輸出結果(返回值)可以被縮小
父類的一個方法的返回值是一個類型T,子類的相同方法(重載或者重寫)的返回值為S,那么里氏替換原則就要求S必須小于等于T。
總結
有子類出現的地方父類未必就可以出現
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的设计模式:里氏替换原则的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【工具】VirtualBox装VBoxG
- 下一篇: 机器学习算法总结--提升方法