深入瞭解LOD

從類關係說起一文中,我們談到了幾種類與類之間的關係,在此來深入一下對象與對象之間的通信問題.爲什麼要深入對象與對象之間的通信呢,其根本在於,系統中不會存在唯一的對象,不同的對象勢必要相互進行交流.


初學者的問題

在開始介紹迪米特法則之前,先讓我們會到初學編程的時代。
在我們剛開始學習編程的時候,通常會將所有的方法都聲明爲公有(public),但隨着我們代碼量的增加,我們都會遇到一個典型的問題:

在調用某個對象的方法時,我們發現編譯器提示這個對象所有的方法,這意味着該對象處在不安全的狀態。爲什麼這麼說呢?如果我們將這個對象比作一個人,那麼這個人在別人面前是赤裸的,沒有任何隱私,這讓別人有機會觀察你的一切行爲,並某刻致命一擊。除此之外,這個完全暴露的人,也會讓別人不知所措。

這顯然不是我們想要的,因此我們需要某種機制來限制的對象信息的公開:哪些信息是可以公開的,哪些是不可以公開的,在java中,我們通過方法的權限來實現這一點,比如private修飾的方法只有對象自己內部可以調用,public修飾的方法是公開給其他對象的等。

現在,你可能已經明白,java的設計者爲什麼要“多此一舉”的爲方法設計權限了。那麼有人會問,我該怎麼確定哪個方法應該被設計成公有的,哪些又應該被設計成私有的呢?

當你心裏有這個疑問的時候,說明你已經開始關注我們經常提到的面向對象編程的原則之一:封裝,即如何劃分對象的結構。
我們都知道對象的結構的可被劃分爲靜態屬性和動態屬性,所謂的靜態屬性就是值對象固有的屬性,比如任何一個生命體都有年齡,而動態屬性也稱爲行爲屬性,指的是對象所表現出來的行爲,比如袋鼠能跳,能呼吸等。而這靜態屬性和動態屬性又可以細分爲可公開的靜態屬性,可公開的動態屬性等。也就是說,劃分對象的結構實則就是確定某個對象的動態屬性和靜態屬性,在此基礎上再來確定屬性是否可公開等。

不難發現,這個過程和我們的認知的思維過程很類似:大腦試圖從各種各樣的的物體中抽取特徵。比如,我們看到貓,狗,仙人掌,爲了能區分它們,我們的大腦會對這三者進行特徵抽取,比如貓和狗都可以移動,有眼睛,會叫,有爪子,而仙人掌則是不可移動,有刺,不能叫等,通過這種特種抽取,我們能區分出動物和植物的區別。換言之,我們之所以能區分出不同的物體,都是因爲我們的大腦已經默默的爲我們做了特徵抽取的工作,這個過程如果由我們主動去做就稱之爲抽象編程。


揭祕迪米特法則

迪米特法則(Law of demeter,縮寫是LOD)要求:一個對象應該對其他對象保持最少了解, 通縮的講就是一個類對自己依賴的類知道的越少越好,也就是對於被依賴的類,向外公開的方法應該儘可能的少。

迪米特法則還有一種解釋:Only talk to your immediate friends,即只與直接朋友通信.
首先來解釋編程中的朋友:兩個對象之間的耦合關係稱之爲朋友,通常有依賴,關聯,聚合和組成等.而直接朋友則通常表現爲關聯,聚合和組成關係,即兩個對象之間聯繫更爲緊密,通常以成員變量,方法的參數和返回值的形式出現.
那麼爲什麼說是要與直接朋友通信呢?觀察直接朋友出現的地方,我們發現在直接朋友出現的地方,大部分情況下可以接口或者父類來代替,可以增加靈活性.
(需要注意,在考慮這個問題的時候,我們只考慮新增的類,而忽視java爲我們提供的基礎類.)

不難發現,迪米特法則強調了一下兩點:

  • 第一要義:從被依賴者的角度來說:只暴露應該暴露的方法或者屬性,即在編寫相關的類的時候確定方法/屬性的權限
  • 第二要義:從依賴者的角度來說,只依賴應該依賴的對象

實例演示

先來解釋第一點,我們使用計算機來說明,考慮計算機關機的過程.當我們按下計算機的關機按鈕的時候,計算機會執行一些列的動作會被執行:比如保存當前未完成的任務,然後是關閉相關的服務,接着是關閉顯示器,最後是關閉電源,這一系列的操作以此完成後,計算機纔會正式被關閉。
我們來用簡單的代碼表示這個過程,在不考慮迪米特法則情況下,我們可能寫出以下代碼

//計算機類
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(){
      //現在你要開始關閉計算機了,正常來說你只需要調用close()方法即可,
      //但是你發現Computer所有的方法都是公開的,該怎麼關閉呢?於是你寫下了以下關閉的流程:

        c.saveCurrentTask();
        c.closePower();
        c.close();

        //亦或是以下的操作:

        c.closePower();


        //還可能是以下的操作
        c.close();
        c.closePower();
    }


}

發現上面的代碼中的問題了沒?觀察clickCloseButton()方法,我們發現這個方法無法編寫:c是一個完全暴露的對象,其方法是完全公開的,那麼對於Person來說,當他想要執行關閉的時候,卻發現不知道該怎麼操作:該調用什麼方法?靠運氣猜麼?如果Person的對象是個不按常理出牌的,那這個Computer的對象豈不是要被搞壞麼?

迪米特法則第一要義

現在我們來看看迪米特法則的第一點:從被依賴者的角度,只應該暴露應該暴露的方法。那麼這裏的c對象應該哪些方法應該是被暴露的呢?很顯然,對於Person來說,只需要關注計算機的關閉操作,而不關心計算機會如何處理這個關閉操作,因此只需要暴露close()方法即可。
那麼上述的代碼應該被修改爲:

//計算機類
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();
    }

}

看一下它的類圖:
這裏寫圖片描述

接下來,我們繼續來看迪米特法則的第二層含義:從依賴者的角度來說,只依賴應該依賴的對象。
這句話令人有點困惑,什麼叫做應該依賴的對象呢?
還是用上面“關閉計算機”的例子來說明:
準確的說,計算機包括操作系統和相關硬件,我們可以將其劃分爲System對象和Container對象。當我們關閉計算機的時候,本質上是向操作系統發出了關機指令,而實則我們只是按了一下關機按鈕,也就是我們並沒有依賴System的對象,而是依賴了Container。這裏Container就是我們上面所說的直接朋友—只和直接朋友通信.

再來深入討論一下這點:
only talk to your immedate friends
這句話只說明了要和直接朋友通信,但是我覺得這還不完整,我更願意將其補充爲:
make sure your friends,only talk to your immedate friends,don't speak to strangers.

大意是:確定你真正的朋友,並只和他們通信,並且不要和陌生人講話.這樣做有個很大的好處就是,能夠簡化對象與對象之間的通信,進而減輕依賴,提供更高的靈活性,當然也可以提供一定的安全性.

現在來想想現實世界中的這麼一種情況:你是一個令人矚目的公衆人物,周圍的每個人都知道你的名字,當你獨自走在大街上的時候會是怎麼樣的一種場景?每個人都想要和你聊天!,和你交換信息!!接着,你發現自己已經寸步難行了.如果這時候你有一個經紀人,來幫你應對周圍的人,而你就只和這個經紀人通信,這樣就大大減輕了你的壓力,不是麼?此時,這個經濟人就相當於你的直接朋友.

迪米特法則第二要義

現在,我們再回顧”關機計算機”這個操作,前面的代碼只是遵從了第一要義,,現在我們結合第二要義來進行改進:System和Container相比,System並非Person的直接朋友,而Container纔是(Person直接打交道的是Container).因此我們需要將原有的Computer拆分成System和Cotainer,然後使Person只與Container通信,因此代碼修改爲:

//System
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();
    }
}

//Contanier
public class Container{
    private System mSystem;

    public void sendCloseCommand(){
        mSystem.close();
    }
}

//Person
ublic class Person{
    private Container c;
    ....

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

}

來看一下它的類圖:
這裏寫圖片描述

延伸的第二要義

在上文中,我們還提到,直接朋友出現的地方,我們可以採用其接口或者父類來代替.那麼在這裏,我們就可以爲Container和System提供相應的接口

//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();
    }

}

來看一下它的類圖:
這裏寫圖片描述

對比這兩種方案,明顯這種方案二的解耦程度更高,靈活大大增強.不難發現,這應用了我們前面提到的依賴倒置,即面向接口編程.

除此之外,我們發現隨着不斷的改進,類的數量也在不斷的增加,從2個增加到5個,這意味着爲了解耦和提高靈活性通常要編寫的類的數量會翻倍.因此,你需要在這做一個權衡,切莫刻意爲了追求設計,而導致整個系統非常的冗餘,最終可能得不償失.


總結

有人會覺得Container像是一個中介(代理).沒錯,我們確實可以稱其爲中介,但這並不能否認他是我們的直接朋友:在很多情況下,中介可以說是我們的一種代表,因此將其定義爲直接朋友是沒有任何問題的.比如,當你想要租房的時候,你可以找房屋中介,對方會按照你的標準爲你尋找合適的住房.

但是問題來了:那麼做一件事情需要多少中介呢?總不能是我委託一箇中介A幫我找房子,但中介A又委託了中介B,中介B又委託了中介C….等等,如果真的是這樣,那還不如我自己去找房子效率更高.在實際開發中,委託的層次要控制在6層以下,多餘6層以上的會使得系統過分的冗餘和並切會委託層次過多而導致開發人員無法正確的理解流程,產生風險的可能會大大提高.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章