聊聊設(shè)計(jì)模式中的里式替換
一、前言
在面向?qū)ο蟮能浖O(shè)計(jì)中,只有盡量降低各個(gè)模塊之間的耦合度,才能提高代碼的復(fù)用率,系統(tǒng)的可維護(hù)性、可擴(kuò)展性才能提高。面向?qū)ο蟮能浖O(shè)計(jì)中,有23種經(jīng)典的設(shè)計(jì)模式,是一套前人代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié),如果把設(shè)計(jì)模式比作武功招式,那么設(shè)計(jì)原則就好比是內(nèi)功心法。常用的設(shè)計(jì)原則有七個(gè),具體如下:
- 單一職責(zé)原則:專注降低類的復(fù)雜度,實(shí)現(xiàn)類要職責(zé)單一;
 - 開放關(guān)閉原則:所有面向?qū)ο笤瓌t的核心,設(shè)計(jì)要對(duì)擴(kuò)展開發(fā),對(duì)修改關(guān)閉;
 - 里式替換原則:實(shí)現(xiàn)開放關(guān)閉原則的重要方式之一,設(shè)計(jì)不要破壞繼承關(guān)系;
 - 依賴倒置原則:系統(tǒng)抽象化的具體實(shí)現(xiàn),要求面向接口編程,是面向?qū)ο笤O(shè)計(jì)的主要實(shí)現(xiàn)機(jī)制之一;
 - 接口隔離原則:要求接口的方法盡量少,接口盡量細(xì)化;
 - 迪米特法則:降低系統(tǒng)的耦合度,使一個(gè)模塊的修改盡量少的影響其他模塊,擴(kuò)展會(huì)相對(duì)容易;
 - 組合復(fù)用原則:在軟件設(shè)計(jì)中,盡量使用組合/聚合而不是繼承達(dá)到代碼復(fù)用的目的。
 
這些設(shè)計(jì)原則并不說(shuō)我們一定要遵循他們來(lái)進(jìn)行設(shè)計(jì),而是根據(jù)我們的實(shí)際情況去怎么去選擇使用他們,來(lái)讓我們的程序做的更加的完善。
之前我們看了單一職責(zé)原則和開閉原則,今天我們一起來(lái)聊聊里式替換原則和依賴倒置原則,千萬(wàn)別小看這些設(shè)計(jì)原則,他在設(shè)計(jì)模式中會(huì)有很多體現(xiàn),所以理解好設(shè)計(jì)原則之后,那么設(shè)計(jì)模式,也會(huì)讓你更加的好理解一點(diǎn)。
二、里式替換原則
里式替換原則,簡(jiǎn)單的說(shuō):如果對(duì)每一個(gè)類型為T1的對(duì)象o1,都有類型為T2的對(duì)象o2,使得以T1定義的所有程序P在所有的對(duì)象o1都代換成o2 時(shí),程序P的行為沒(méi)有發(fā)生變化,那么類型 T2 是類型 T1 的子類型。
換句話來(lái)說(shuō),一個(gè)軟件實(shí)體如果使用一個(gè)基類的話,那么一定適用于其子類,而且它根本不會(huì)察覺(jué)出基類對(duì)象和子類對(duì)象的區(qū)別。
比如說(shuō),假設(shè)有兩個(gè)類,一個(gè)是Base類,另一個(gè)是Derived類,并且Derived類是Base的子類,那么一個(gè)方法如果可以接受一個(gè)基類對(duì)象b的話:method(Base b),那么它必然可以接受一個(gè)子類對(duì)象d,可以有 method1(d)。
里式替換原則是繼承復(fù)用的基石,只有當(dāng)衍生類可以替換掉基類,軟件單位的功能不會(huì)受到影響的時(shí)候,基類才能真正被復(fù)用,而衍生類也才能夠在基類的基礎(chǔ)上增加新的行為。
我們通過(guò)一個(gè)例子來(lái)理解一下:
《西游記》中,美猴王下地府橋段,個(gè)位應(yīng)該有印象把,到達(dá)閻王殿之后,拿到生死簿,把生死簿上所有的包括自己,還有其他的獼猴,所有的猴子猴算都給劃了,這也是導(dǎo)致之后真假美猴王橋段的前序。
畫個(gè)圖理解:
圖片
很顯然,地府管理一切生靈的生死的方法都是通過(guò)類來(lái)進(jìn)行區(qū)分的,比如孫悟空就是石猴,之后出現(xiàn)的那個(gè)六耳獼猴就是獼猴,但是他們都是屬于同一個(gè)類,猴類,就像下圖中。
圖片
因此,孫悟空把猴類中有姓名的都從生死簿勾掉之后,顯然是因?yàn)楣椿晷」韨儾⒉粎^(qū)分石猴類與獼猴類,就像下圖:
圖片
換句話來(lái)說(shuō),只要是猴類適用的,獼猴和石猴都適用,這其實(shí)就是里式替換原則。
這是第一種解釋,還有第二個(gè)更加通俗易懂的解釋:所有引用基類的地方必須能透明地使用其子類的對(duì)象。
第二種定義比較通俗,容易理解:只要有父類出現(xiàn)的地方,都可以用子類來(lái)替代,而且不會(huì)出現(xiàn)任何錯(cuò)誤和異常。但是反過(guò)來(lái)則不行,有子類出現(xiàn)的地方,不能用其父類替代。
public class TestA {
    public void fun(int a,int b){
        System.out.println(a+"+"+b+"="+(a+b));
    }
    public static void main(String[] args) {
        System.out.println("父類的運(yùn)行結(jié)果");
        TestA a=new TestA();
        a.fun(1,2);
        //父類存在的地方,可以用子類替代
        //子類B替代父類A
        System.out.println("子類替代父類后的運(yùn)行結(jié)果");
        TestB b=new TestB();
        b.fun(1,2);
    }
}
class TestB extends TestA{
    @Override
    public void fun(int a, int b) {
        System.out.println(a+"-"+b+"="+(a-b));
    }
}大家肯定也都能猜出來(lái)結(jié)果是什么樣子的。
父類的運(yùn)行結(jié)果
1+2=3
子類替代父類后的運(yùn)行結(jié)果
1-2=-1
Process finished with exit code 0我們想要的結(jié)果是“1+2=3”??梢钥吹剑椒ㄖ貙懞蠼Y(jié)果就不是了我們想要的結(jié)果了,也就是這個(gè)程序中子類B不能替代父類A。這違反了里氏替換原則原則,從而給程序造成了錯(cuò)誤。
子類中可以增加自己特有的方法
這個(gè)很容易理解,子類繼承了父類,擁有了父類和方法,同時(shí)還可以定義自己有,而父類沒(méi)有的方法。這是在繼承父類方法的基礎(chǔ)上進(jìn)行功能的擴(kuò)展,符合里氏替換原則。
public class TestA {
    public void fun(int a,int b){
        System.out.println(a+"+"+b+"="+(a+b));
    }
    public static void main(String[] args) {
        System.out.println("父類的運(yùn)行結(jié)果");
        TestA a=new TestA();
        a.fun(1,2);
        //父類存在的地方,可以用子類替代
        //子類B替代父類A
        System.out.println("子類替代父類后的運(yùn)行結(jié)果");
        TestB b=new TestB();
        b.fun(1,2);
        b.newFun();
    }
}
class TestB extends TestA{
    public void newFun(){
        System.out.println("這是子類的新方法...");
    }
}這次運(yùn)行出來(lái)的代碼結(jié)果就是我們意料中的內(nèi)容了。
父類的運(yùn)行結(jié)果
1+2=3
子類替代父類后的運(yùn)行結(jié)果
1+2=3
這是子類的新方法...
Process finished with exit code 0JAVA語(yǔ)言對(duì)里式替換原則支持的局限:
JAVA編譯器的檢查是有局限性的,為什么呢?舉個(gè)例子來(lái)說(shuō),描述一個(gè)物體大小的量有精度和準(zhǔn)確度兩種屬性。所謂的精度,就是這個(gè)量的有效數(shù)字有多少位;而所謂的精準(zhǔn)度,是這個(gè)量與真實(shí)的物體大小相符合到什么程度。
一個(gè)量可以有很高的精度,但是卻無(wú)法與真實(shí)物體的情況相吻合,JAVA語(yǔ)言編譯器能夠檢查的,僅僅是相當(dāng)于精度的屬性而已,它沒(méi)有辦法去檢查這個(gè)量與真實(shí)物體的差距。
換一句話來(lái)說(shuō),JAVA編譯器不能檢查一個(gè)系統(tǒng)在實(shí)現(xiàn)和商業(yè)邏輯上是否滿足里式替換原則。
三、依賴倒置原則
如果說(shuō)實(shí)現(xiàn)開閉原則的關(guān)鍵事抽象化,是面向?qū)ο笤O(shè)計(jì)的目標(biāo)的話,依賴倒置原則就是這個(gè)面向?qū)ο笤O(shè)計(jì)的主要機(jī)制。
依賴倒置原則,簡(jiǎn)單的說(shuō):抽象不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)當(dāng)依賴于抽象。換言之,要針對(duì)接口編程,而不是針對(duì)實(shí)現(xiàn)編程。
為什么要實(shí)現(xiàn)倒置?這也是我們看這個(gè)定義的時(shí)候產(chǎn)生的一些問(wèn)題,那么我們就來(lái)說(shuō)說(shuō)。
簡(jiǎn)單的來(lái)說(shuō),傳統(tǒng)的過(guò)程性系統(tǒng)的設(shè)計(jì)辦法傾向于使高層次的模塊依賴于低層次的模塊,抽象層依賴于具體層次,倒置原則是要把這個(gè)錯(cuò)誤的依賴關(guān)系倒轉(zhuǎn)過(guò)來(lái),這就是依賴倒置原則的由來(lái)。也是為什么要進(jìn)行依賴倒置。
依賴倒置原則的實(shí)現(xiàn)方法
依賴倒置原則的目的是通過(guò)要面向接口的編程來(lái)降低類間的耦合性,所以我們?cè)趯?shí)際編程中只要遵循以下4點(diǎn),就能在項(xiàng)目中滿足這個(gè)規(guī)則:
- 每個(gè)類盡量提供接口或抽象類,或者兩者都具備。
 - 變量的聲明類型盡量是接口或者是抽象類。
 - 任何類都不應(yīng)該從具體類派生。
 - 用繼承時(shí)盡量遵循里氏替換原則。
 
下面我們通過(guò)一些代碼實(shí)例(商品售賣)來(lái)進(jìn)行理解:
class BeijingShop implements Shop{
        public String sell(){
            return "北京商店售賣:北京烤鴨,稻香村月餅";
        }
    }
    class ShanDongShop implements  Shop{
        @Override
        public String sell() {
            return "山東商店售賣:德州扒雞,煙臺(tái)蘋果";
        }
    }
    //如果說(shuō)顧客去購(gòu)買商品
class Customer{
    public void shopping(ShanDongShop shop){
        //購(gòu)物
        System.out.println(shop.sell());
    }
}
//這是在山東商店購(gòu)買,如果說(shuō)是在北京商店購(gòu)買就會(huì)這樣
class Customer{
    public void shopping(BeijingShop shop) {
        //購(gòu)物
        System.out.println(shop.sell());
    }
}這也是這種設(shè)計(jì)的存在缺陷,顧客每更換一家商店,都要修改一次代碼,這明顯違背了開閉原則。存在以上缺點(diǎn)的原因是:顧客類設(shè)計(jì)時(shí)同具體的商店類綁定了,這違背了依賴倒置原則。解決方式我們可以定義一個(gè)共同的接口Shop,就可以這樣了。
public class TestSale {
    public static void main(String[] args) {
        Customer c = new Customer();
        System.out.println("---顧客購(gòu)買商品如下---");
        c.shopping(new ShanDongShop());
        c.shopping(new BeijingShop());
    }
}
interface Shop{
    //售賣方法
    public String sell();
}
class BeijingShop implements Shop{
    public String sell(){
        return "北京商店售賣:北京烤鴨,稻香村月餅";
    }
}
class ShanDongShop implements  Shop{
    @Override
    public String sell() {
        return "山東商店售賣:德州扒雞,煙臺(tái)蘋果";
    }
}
class Customer{
    public void shopping(Shop shop) {
        System.out.println(shop.sell());//購(gòu)物
    }
}程序運(yùn)行結(jié)果:
---顧客購(gòu)買商品如下---
山東商店售賣:德州扒雞,煙臺(tái)蘋果
北京商店售賣:北京烤鴨,稻香村月餅
Process finished with exit code 0這樣,不管顧客類 Customer 訪問(wèn)什么商店,或者增加新的商店,都不需要修改原有代碼了。
依賴倒置原則是OO設(shè)計(jì)的核心原則,設(shè)計(jì)模式的研究和應(yīng)用是以依賴導(dǎo)致原則為知道原則的,在知識(shí)星球中的設(shè)計(jì)模式中我們將會(huì)一一給大家體現(xiàn)。















 
 
 













 
 
 
 