白話設(shè)計(jì)——淺談迪米特法則

前面我們談到了幾種類與類之間的關(guān)系,現(xiàn)在我們來(lái)深入一下對(duì)象與對(duì)象之間的通信問(wèn)題.
為什么要深入對(duì)象與對(duì)象之間的通信呢,其根本在于,系統(tǒng)中不會(huì)存在唯一的對(duì)象,不同的對(duì)象勢(shì)必要相互進(jìn)行交流.


初學(xué)者的問(wèn)題

在我們剛開(kāi)始學(xué)習(xí)編程的時(shí)候,通常會(huì)將所有的方法都聲明為公有(public),但隨著我們代碼量的增加,我們都會(huì)遇到一個(gè)典型的問(wèn)題:

在調(diào)用某個(gè)對(duì)象的方法時(shí),我們發(fā)現(xiàn)編譯器提示這個(gè)對(duì)象所有的方法,這意味著該對(duì)象處在不安全的狀態(tài)。為什么這么說(shuō)呢?如果我們將這個(gè)對(duì)象比作一個(gè)人,那么這個(gè)人在別人面前是赤裸的,沒(méi)有任何隱私,這讓別人有機(jī)會(huì)觀察你的一切行為,并某刻致命一擊。除此之外,這個(gè)完全暴露的人,也會(huì)讓別人不知所措。

這顯然不是我們想要的,因此我們需要某種機(jī)制來(lái)限制的對(duì)象信息的公開(kāi):哪些信息是可以公開(kāi)的,哪些是不可以公開(kāi)的,在java中,我們通過(guò)方法的權(quán)限來(lái)實(shí)現(xiàn)這一點(diǎn),比如private修飾的方法只有對(duì)象自己內(nèi)部可以調(diào)用,public修飾的方法是公開(kāi)給其他對(duì)象的等。

現(xiàn)在,你可能已經(jīng)明白,java的設(shè)計(jì)者為什么要“多此一舉”的為方法設(shè)計(jì)權(quán)限了。那么有人會(huì)問(wèn),我該怎么確定哪個(gè)方法應(yīng)該被設(shè)計(jì)成公有的,哪些又應(yīng)該被設(shè)計(jì)成私有的呢?

當(dāng)你心里有這個(gè)疑問(wèn)的時(shí)候,說(shuō)明你已經(jīng)開(kāi)始關(guān)注我們經(jīng)常提到的面向?qū)ο缶幊痰脑瓌t之一:封裝,即如何劃分對(duì)象的結(jié)構(gòu)。
我們都知道對(duì)象的結(jié)構(gòu)的可被劃分為靜態(tài)屬性和動(dòng)態(tài)屬性,所謂的靜態(tài)屬性就是值對(duì)象固有的屬性,比如任何一個(gè)生命體都有年齡,而動(dòng)態(tài)屬性也稱為行為屬性,指的是對(duì)象所表現(xiàn)出來(lái)的行為,比如袋鼠能跳,能呼吸等。而這靜態(tài)屬性和動(dòng)態(tài)屬性又可以細(xì)分為可公開(kāi)的靜態(tài)屬性,可公開(kāi)的動(dòng)態(tài)屬性等。也就是說(shuō),劃分對(duì)象的結(jié)構(gòu)實(shí)則就是確定某個(gè)對(duì)象的動(dòng)態(tài)屬性和靜態(tài)屬性,在此基礎(chǔ)上再來(lái)確定屬性是否可公開(kāi)等。

不難發(fā)現(xiàn),這個(gè)過(guò)程和我們的認(rèn)知的思維過(guò)程很類似:大腦試圖從各種各樣的的物體中抽取特征。比如,我們看到貓,狗,仙人掌,為了能區(qū)分它們,我們的大腦會(huì)對(duì)這三者進(jìn)行特征抽取,比如貓和狗都可以移動(dòng),有眼睛,會(huì)叫,有爪子,而仙人掌則是不可移動(dòng),有刺,不能叫等,通過(guò)這種特種抽取,我們能區(qū)分出動(dòng)物和植物的區(qū)別。換言之,我們之所以能區(qū)分出不同的物體,都是因?yàn)槲覀兊拇竽X已經(jīng)默默的為我們做了特征抽取的工作,這個(gè)過(guò)程如果由我們主動(dòng)去做就稱之為抽象編程。


揭秘迪米特法則

迪米特法則(Law of demeter,縮寫(xiě)是LOD)要求:一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持最少了解, 通縮的講就是一個(gè)類對(duì)自己依賴的類知道的越少越好,也就是對(duì)于被依賴的類,向外公開(kāi)的方法應(yīng)該盡可能的少。

迪米特法則還有一種解釋:Only talk to your immediate friends,即只與直接朋友通信.首先來(lái)解釋編程中的朋友:兩個(gè)對(duì)象之間的耦合關(guān)系稱之為朋友,通常有依賴,關(guān)聯(lián),聚合和組成等.而直接朋友則通常表現(xiàn)為關(guān)聯(lián),聚合和組成關(guān)系,即兩個(gè)對(duì)象之間聯(lián)系更為緊密,通常以成員變量,方法的參數(shù)和返回值的形式出現(xiàn).

那么為什么說(shuō)是要與直接朋友通信呢?觀察直接朋友出現(xiàn)的地方,我們發(fā)現(xiàn)在直接朋友出現(xiàn)的地方,大部分情況下可以接口或者父類來(lái)代替,可以增加靈活性.
(需要注意,在考慮這個(gè)問(wèn)題的時(shí)候,我們只考慮新增的類,而忽視java為我們提供的基礎(chǔ)類.)

實(shí)例演示

不難發(fā)現(xiàn),迪米特法則強(qiáng)調(diào)了一下兩點(diǎn):

  • 第一要義:從被依賴者的角度來(lái)說(shuō):只暴露應(yīng)該暴露的方法或者屬性,即在編寫(xiě)相關(guān)的類的時(shí)候確定方法/屬性的權(quán)限
  • 第二要義:從依賴者的角度來(lái)說(shuō),只依賴應(yīng)該依賴的對(duì)象

先來(lái)解釋第一點(diǎn),我們使用計(jì)算機(jī)來(lái)說(shuō)明,以關(guān)閉計(jì)算機(jī)為例:

當(dāng)我們按下計(jì)算機(jī)的關(guān)機(jī)按鈕的時(shí)候,計(jì)算機(jī)會(huì)執(zhí)行一些列的動(dòng)作會(huì)被執(zhí)行:比如保存當(dāng)前未完成的任務(wù),然后是關(guān)閉相關(guān)的服務(wù),接著是關(guān)閉顯示器,最后是關(guān)閉電源,這一系列的操作以此完成后,計(jì)算機(jī)才會(huì)正式被關(guān)閉。

現(xiàn)在,我們來(lái)用簡(jiǎn)單的代碼表示這個(gè)過(guò)程,在不考慮迪米特法則情況下,我們可能寫(xiě)出以下代碼

//計(jì)算機(jī)類
public class Computer{

    public void saveCurrentTask(){
        //do something
    }
    public void closeService(){
        //do something
    }
    public void closeScreen(){
        //do something
    }
    
    public void closePower(){
        //do something
    }
    
    public void close(){
        saveCurrentTask();
        closeService();
        closeScreen();
        closePower();
    }
}

//人
public class Person{
    private Computer c;
    
    ...
    
    public void clickCloseButton(){
      //現(xiàn)在你要開(kāi)始關(guān)閉計(jì)算機(jī)了,正常來(lái)說(shuō)你只需要調(diào)用close()方法即可,
      //但是你發(fā)現(xiàn)Computer所有的方法都是公開(kāi)的,該怎么關(guān)閉呢?于是你寫(xiě)下了以下關(guān)閉的流程:        
        c.saveCurrentTask();
        c.closePower();
        c.close();
        
        //亦或是以下的操作        
        c.closePower();
        
        //還可能是以下的操作
        c.close();
        c.closePower();
    }

}

發(fā)現(xiàn)上面的代碼中的問(wèn)題了沒(méi)?
我們觀察clickCloseButton()方法,我們發(fā)現(xiàn)這個(gè)方法無(wú)法編寫(xiě):c是一個(gè)完全暴露的對(duì)象,其方法是完全公開(kāi)的,那么對(duì)于Person來(lái)說(shuō),當(dāng)他想要執(zhí)行關(guān)閉的時(shí)候,卻發(fā)現(xiàn)不知道該怎么操作:該調(diào)用什么方法?靠運(yùn)氣猜么?如果Person的對(duì)象是個(gè)不按常理出牌的,那這個(gè)Computer的對(duì)象豈不是要被搞壞么?

迪米特法則第一要義

現(xiàn)在我們來(lái)看看迪米特法則的第一點(diǎn):從被依賴者的角度,只應(yīng)該暴露應(yīng)該暴露的方法。那么這里的c對(duì)象應(yīng)該哪些方法應(yīng)該是被暴露的呢?很顯然,對(duì)于Person來(lái)說(shuō),只需要關(guān)注計(jì)算機(jī)的關(guān)閉操作,而不關(guān)心計(jì)算機(jī)會(huì)如何處理這個(gè)關(guān)閉操作,因此只需要暴露close()方法即可。
那么上述的代碼應(yīng)該被修改為:

//計(jì)算機(jī)類
public class Computer{

    private void saveCurrentTask(){
        //do something
    }
    private void closeService(){
        //do something
    }
    private void closeScreen(){
        //do something
    }
    
    private void closePower(){
        //do something
    }
    
    public void close(){
        saveCurrentTask();
        closeService();
        closeScreen();
        closePower();
    }
}

//人
public class Person{
    private Computer c;
    ...

    public  void clickCloseButton(){
       c.close();
    }

}

看一下它的類圖:


這里寫(xiě)圖片描述

接下來(lái),我們繼續(xù)來(lái)看迪米特法則的第二層含義:從依賴者的角度來(lái)說(shuō),只依賴應(yīng)該依賴的對(duì)象。
這句話令人有點(diǎn)困惑,什么叫做應(yīng)該依賴的對(duì)象呢?我們還是用上面“關(guān)閉計(jì)算機(jī)”的例子來(lái)說(shuō)明:
準(zhǔn)確的說(shuō),計(jì)算機(jī)包括操作系統(tǒng)和相關(guān)硬件,我們可以將其劃分為System對(duì)象和Container對(duì)象。當(dāng)我們關(guān)閉計(jì)算機(jī)的時(shí)候,本質(zhì)上是向操作系統(tǒng)發(fā)出了關(guān)機(jī)指令,而實(shí)則我們只是按了一下關(guān)機(jī)按鈕,也就是我們并沒(méi)有依賴System的對(duì)象,而是依賴了Container。這里Container就是我們上面所說(shuō)的直接朋友---只和直接朋友通信.

我們就這點(diǎn),繼續(xù)深入討論一下:
only talk to your immedate friends
這句話只說(shuō)明了要和直接朋友通信,但是我覺(jué)得這還不完整,我更愿意將其補(bǔ)充為:
make sure your friends,only talk to your immedate friends,don't speak to strangers.
大意是:確定你真正的朋友,并只和他們通信,并且不要和陌生人講話.這樣做有個(gè)很大的好處就是,能夠簡(jiǎn)化對(duì)象與對(duì)象之間的通信,進(jìn)而減輕依賴,提供更高的靈活性,當(dāng)然也可以提供一定的安全性.

現(xiàn)在來(lái)想想現(xiàn)實(shí)世界中的這么一種情況:你是一個(gè)令人矚目的公眾人物,周圍的每個(gè)人都知道你的名字,當(dāng)你獨(dú)自走在大街上的時(shí)候會(huì)是怎么樣的一種場(chǎng)景?每個(gè)人都想要和你聊天!,和你交換信息!!接著,你發(fā)現(xiàn)自己已經(jīng)寸步難行了.如果這時(shí)候你有一個(gè)經(jīng)紀(jì)人,來(lái)幫你應(yīng)對(duì)周圍的人,而你就只和這個(gè)經(jīng)紀(jì)人通信,這樣就大大減輕了你的壓力,不是么?此時(shí),這個(gè)經(jīng)濟(jì)人就相當(dāng)于你的直接朋友.


迪米特法則第二要義

現(xiàn)在,我們?cè)倩仡?關(guān)機(jī)計(jì)算機(jī)"這個(gè)操作,前面的代碼只是遵從了"暴漏應(yīng)該暴漏的方法"這一點(diǎn),現(xiàn)在我們結(jié)合第二點(diǎn)來(lái)進(jìn)行改進(jìn):System和Container相比,System并非Person的直接朋友,而Container才是(Person直接打交道的是Container).因此我們需要將原有的Computer拆分成System和Cotainer,然后使Person只與Container通信,因此代碼修改為:

//操作系統(tǒng)
public class System{

    private void saveCurrentTask(){
        //do something
    }
    private void closeService(){
        //do something
    }
    private void closeScreen(){
        //do something
    }
    
    private void closePower(){
        //do something
    }
    
    public void close(){
        saveCurrentTask();
        closeService();
        closeScreen();
        closePower();
    }
}

//硬件設(shè)備容器
public class Container{
    private System mSystem;
    
    public void sendCloseCommand(){
        mSystem.close();
    }
}

//人
ublic class Person{
    private Container c;
    ....
    
    public void clickCloseButton(){
       c.sendCloseCommand();
    }

}

來(lái)看一下它的類圖:


這里寫(xiě)圖片描述

重構(gòu),改善既有設(shè)計(jì)

在上文中,我們還提到,直接朋友出現(xiàn)的地方,我們可以采用其接口或者父類來(lái)代替.那么在這里,我們就可以為Container和System提供相應(yīng)的接口

//System interface
public interface ISystem{
    void close();
}

//System
public class System implements ISystem{
    
    private void saveCurrentTask(){
        //do something
    }
    
    private void closeService(){
        //do something
    }
    
    private void closeScreen(){
        //do something
    }
    
    private void closePower(){
        //do something
    }
    
    @override
    public void close(){
        saveCurrentTask();
        closeService();
        closeScreen();
        closePower();
    }
}

//IContainer interface
public interface IContainer{
    void sendCloseCommand();
}

//Contanier
public class Container implements IContainer{
    private System mSystem;
    
    @override
    public void sendCloseCommand(){
        mSystem.close();
    }
}

//Person
ublic class Person{
    private IContainer c;
    ....
    
    public void clickCloseButton(){
       c.sendCloseCommand();
    }

}

來(lái)看一下它的類圖:


這里寫(xiě)圖片描述

對(duì)比這兩種方案,明顯這種方案二的解耦程度更高,靈活大大增強(qiáng).不難發(fā)現(xiàn),這應(yīng)用了我們前面提到的依賴倒置,即面向接口編程.

除此之外,我們發(fā)現(xiàn)隨著不斷的改進(jìn),類的數(shù)量也在不斷的增加,從2個(gè)增加到5個(gè),這意味著為了解耦和提高靈活性通常要編寫(xiě)的類的數(shù)量會(huì)翻倍.因此,你需要在這做一個(gè)權(quán)衡,切莫刻意為了追求設(shè)計(jì),而導(dǎo)致整個(gè)系統(tǒng)非常的冗余,最終可能得不償失.


總結(jié)

有人會(huì)覺(jué)得Container像是一個(gè)中介(代理).沒(méi)錯(cuò),我們確實(shí)可以稱其為中介,但這并不能否認(rèn)他是我們的直接朋友:在很多情況下,中介可以說(shuō)是我們的一種代表,因此將其定義為直接朋友是沒(méi)有任何問(wèn)題的.比如,當(dāng)你想要租房的時(shí)候,你可以找房屋中介,對(duì)方會(huì)按照你的標(biāo)準(zhǔn)為你尋找合適的住房.但是問(wèn)題來(lái)了:那么做一件事情需要多少中介呢?總不能是我委托一個(gè)中介A幫我找房子,但中介A又委托了中介B,中介B又委托了中介C....等等,如果真的是這樣,那還不如我自己去找房子效率更高.在實(shí)際開(kāi)發(fā)中,委托的層次要控制在6層以下,多余6層以上的會(huì)使得系統(tǒng)過(guò)分的冗余和并切會(huì)委托層次過(guò)多而導(dǎo)致開(kāi)發(fā)人員無(wú)法正確的理解流程,產(chǎn)生風(fēng)險(xiǎn)的可能會(huì)大大提高.

到目前,我們已經(jīng)徹底的了解了迪米特法則.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 前面我們談到了幾種類與類之間的關(guān)系,現(xiàn)在我們來(lái)深入一下對(duì)象與對(duì)象之間的通信問(wèn)題.為什么要深入對(duì)象與對(duì)象之間的通信呢...
    涅槃1992閱讀 8,778評(píng)論 4 13
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,836評(píng)論 18 139
  • 設(shè)計(jì)模式基本原則 開(kāi)放-封閉原則(OCP),是說(shuō)軟件實(shí)體(類、模塊、函數(shù)等等)應(yīng)該可以拓展,但是不可修改。開(kāi)-閉原...
    西山薄涼閱讀 3,852評(píng)論 3 14
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 31,738評(píng)論 18 399
  • 今天早晨,放開(kāi)音響,響起的第一首歌《愛(ài)的奉獻(xiàn)》。和媽媽一起唱起了這首歌,有一句歌詞寫(xiě)得好,“只要人人都獻(xiàn)出...
    ad5bf11c46ef閱讀 181評(píng)論 0 0