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