設(shè)計(jì)原則之迪米特法則

1. 定義

每個(gè)模塊只應(yīng)該了解那些與它關(guān)系密切的模塊的有限知識(shí)?;蛘哒f(shuō),每個(gè)模塊只和自己的朋友“說(shuō)話”,不和陌生人“說(shuō)話”。即:不該有直接依賴關(guān)系的類之間,不要有依賴;有依賴關(guān)系的類之間,盡量只依賴必要的接口(也就是定義中的“有限知識(shí)”)。

2. 第一個(gè)例子

我們先來(lái)看這條原則中的前半部分,“不該有直接依賴關(guān)系的類之間,不要有依賴”。我舉個(gè)例子解釋一下。

這個(gè)例子實(shí)現(xiàn)了簡(jiǎn)化版的搜索引擎爬取網(wǎng)頁(yè)的功能。代碼中包含三個(gè)主要的類。其中,NetworkTransporter 類負(fù)責(zé)底層網(wǎng)絡(luò)通信,根據(jù)請(qǐng)求獲取數(shù)據(jù);HtmlDownloader 類用來(lái)通過(guò) URL 獲取網(wǎng)頁(yè);Document 表示網(wǎng)頁(yè)文檔,后續(xù)的網(wǎng)頁(yè)內(nèi)容抽取、分詞、索引都是以此為處理對(duì)象。具體的代碼實(shí)現(xiàn)如下所示:

public class NetworkTransporter {
    // 省略屬性和其他方法...
    public Byte[] send(HtmlRequest htmlRequest) {
      //...
    }
}

public class HtmlDownloader {
  private NetworkTransporter transporter;//通過(guò)構(gòu)造函數(shù)或IOC注入
  
  public Html downloadHtml(String url) {
    Byte[] rawHtml = transporter.send(new HtmlRequest(url));
    return new Html(rawHtml);
  }
}

public class Document {
  private Html html;
  private String url;
  
  public Document(String url) {
    this.url = url;
    HtmlDownloader downloader = new HtmlDownloader();
    this.html = downloader.downloadHtml(url);
  }
  //...
}

這段代碼雖然“能用”,能實(shí)現(xiàn)我們想要的功能,但是它不夠“好用”,有比較多的設(shè)計(jì)缺陷。

2.1 NetworkTransporter

首先,我們來(lái)看 NetworkTransporter 類。作為一個(gè)底層網(wǎng)絡(luò)通信類,我們希望它的功能盡可能通用,而不只是服務(wù)于下載 HTML,所以,我們不應(yīng)該直接依賴太具體的發(fā)送對(duì)象 HtmlRequest。從這一點(diǎn)上講,NetworkTransporter 類的設(shè)計(jì)違背迪米特法則,依賴了不該有直接依賴關(guān)系的 HtmlRequest 類。

我們應(yīng)該把 address 和 content 交給 NetworkTransporter,而非是直接把 HtmlRequest 交給 NetworkTransporter。根據(jù)這個(gè)思路,NetworkTransporter 重構(gòu)之后的代碼如下所示:

public class NetworkTransporter {
    // 省略屬性和其他方法...
    public Byte[] send(String address, Byte[] data) {
      //...
    }
}

2.2 HtmlDownloader

這個(gè)類的設(shè)計(jì)沒(méi)有問(wèn)題。不過(guò),我們修改了 NetworkTransporter 的 send() 函數(shù)的定義,而這個(gè)類用到了 send() 函數(shù),所以我們需要對(duì)它做相應(yīng)的修改,修改后的代碼如下所示:

public class HtmlDownloader {
  private NetworkTransporter transporter;//通過(guò)構(gòu)造函數(shù)或IOC注入
  
  // HtmlDownloader這里也要有相應(yīng)的修改
  public Html downloadHtml(String url) {
    HtmlRequest htmlRequest = new HtmlRequest(url);
    Byte[] rawHtml = transporter.send(
      htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
    return new Html(rawHtml);
  }
}

2.3 Document

這個(gè)類的問(wèn)題比較多,主要有三點(diǎn)。

  • 第一,構(gòu)造函數(shù)中的 downloader.downloadHtml() 邏輯復(fù)雜,耗時(shí)長(zhǎng),不應(yīng)該放到構(gòu)造函數(shù)中,會(huì)影響代碼的可測(cè)試性。
  • 第二,HtmlDownloader 對(duì)象在構(gòu)造函數(shù)中通過(guò) new 來(lái)創(chuàng)建,違反了基于接口而非實(shí)現(xiàn)編程的設(shè)計(jì)思想,也會(huì)影響到代碼的可測(cè)試性。
  • 第三,從業(yè)務(wù)含義上來(lái)講,Document 網(wǎng)頁(yè)文檔沒(méi)必要依賴 HtmlDownloader 類,違背了迪米特法則。

雖然 Document 類的問(wèn)題很多,但修改起來(lái)比較簡(jiǎn)單,只要一處改動(dòng)就可以解決所有問(wèn)題。修改之后的代碼如下所示:

public class Document {
  private Html html;
  private String url;
  
  public Document(String url, Html html) {
    this.html = html;
    this.url = url;
  }
  //...
}

// 通過(guò)一個(gè)工廠方法來(lái)創(chuàng)建Document
public class DocumentFactory {
  private HtmlDownloader downloader;
  
  public DocumentFactory(HtmlDownloader downloader) {
    this.downloader = downloader;
  }
  
  public Document createDocument(String url) {
    Html html = downloader.downloadHtml(url);
    return new Document(url, html);
  }
}

3. 第二個(gè)例子

現(xiàn)在,我們?cè)賮?lái)看一下這條原則中的后半部分:“有依賴關(guān)系的類之間,盡量只依賴必要的接口”。我們還是結(jié)合一個(gè)例子來(lái)講解。下面這段代碼非常簡(jiǎn)單,Serialization 類負(fù)責(zé)對(duì)象的序列化和反序列化。

public class Serialization {
  public String serialize(Object object) {
    String serializedResult = ...;
    //...
    return serializedResult;
  }
  
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    //...
    return deserializedResult;
  }
}

單看這個(gè)類的設(shè)計(jì),沒(méi)有一點(diǎn)問(wèn)題。不過(guò),如果我們把它放到一定的應(yīng)用場(chǎng)景里,那就還有繼續(xù)優(yōu)化的空間。假設(shè)在我們的項(xiàng)目中,有些類只用到了序列化操作,而另一些類只用到反序列化操作。那基于迪米特法則后半部分“有依賴關(guān)系的類之間,盡量只依賴必要的接口”,只用到序列化操作的那部分類不應(yīng)該依賴反序列化接口。同理,只用到反序列化操作的那部分類不應(yīng)該依賴序列化接口。

根據(jù)這個(gè)思路,我們應(yīng)該將 Serialization 類拆分為兩個(gè)更小粒度的類,一個(gè)只負(fù)責(zé)序列化(Serializer 類),一個(gè)只負(fù)責(zé)反序列化(Deserializer 類)。拆分之后,使用序列化操作的類只需要依賴 Serializer 類,使用反序列化操作的類只需要依賴 Deserializer 類。拆分之后的代碼如下所示:

public class Serializer {
  public String serialize(Object object) {
    String serializedResult = ...;
    ...
    return serializedResult;
  }
}

public class Deserializer {
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

不知道你有沒(méi)有看出來(lái),盡管拆分之后的代碼更能滿足迪米特法則,但卻違背了高內(nèi)聚的設(shè)計(jì)思想。高內(nèi)聚要求相近的功能要放到同一個(gè)類中,這樣可以方便功能修改的時(shí)候,修改的地方不至于過(guò)于分散。對(duì)于剛剛這個(gè)例子來(lái)說(shuō),如果我們修改了序列化的實(shí)現(xiàn)方式,比如從 JSON 換成了 XML,那反序列化的實(shí)現(xiàn)邏輯也需要一并修改。在未拆分的情況下,我們只需要修改一個(gè)類即可。在拆分之后,我們需要修改兩個(gè)類。顯然,這種設(shè)計(jì)思路的代碼改動(dòng)范圍變大了。

如果我們既不想違背高內(nèi)聚的設(shè)計(jì)思想,也不想違背迪米特法則,那我們?cè)撊绾谓鉀Q這個(gè)問(wèn)題呢?實(shí)際上,通過(guò)引入兩個(gè)接口就能輕松解決這個(gè)問(wèn)題,具體的代碼如下所示。

public interface Serializable {
  String serialize(Object object);
}

public interface Deserializable {
  Object deserialize(String text);
}

public class Serialization implements Serializable, Deserializable {
  @Override
  public String serialize(Object object) {
    String serializedResult = ...;
    ...
    return serializedResult;
  }
  
  @Override
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

public class DemoClass_1 {
  private Serializable serializer;
  
  public Demo(Serializable serializer) {
    this.serializer = serializer;
  }
  //...
}

public class DemoClass_2 {
  private Deserializable deserializer;
  
  public Demo(Deserializable deserializer) {
    this.deserializer = deserializer;
  }
  //...
}

4. 高內(nèi)聚、低耦合

利用迪米特法則這個(gè)原則,能夠幫我們實(shí)現(xiàn)代碼的“高內(nèi)聚、松耦合”。那么什么是“高內(nèi)聚、低耦合”呢?

4.1 定義

“高內(nèi)聚、低耦合”是一個(gè)非常重要的設(shè)計(jì)思想,能夠有效地提高代碼的可讀性和可維護(hù)性,縮小功能改動(dòng)導(dǎo)致的代碼改動(dòng)范圍。實(shí)際上,在前面的章節(jié)中,我們已經(jīng)多次提到過(guò)這個(gè)設(shè)計(jì)思想。很多設(shè)計(jì)原則都以實(shí)現(xiàn)代碼的“高內(nèi)聚、低耦合”為目的,比如單一職責(zé)原則、基于接口而非實(shí)現(xiàn)編程等。

實(shí)際上,“高內(nèi)聚、低耦合”是一個(gè)比較通用的設(shè)計(jì)思想,可以用來(lái)指導(dǎo)不同粒度代碼的設(shè)計(jì)與開(kāi)發(fā),比如系統(tǒng)、模塊、類,甚至是函數(shù),也可以應(yīng)用到不同的開(kāi)發(fā)場(chǎng)景中,比如微服務(wù)、框架、組件、類庫(kù)等。

4.2 高內(nèi)聚

所謂高內(nèi)聚,就是指相近的功能應(yīng)該放到同一個(gè)類中,不相近的功能不要放到同一個(gè)類中。相近的功能往往會(huì)被同時(shí)修改,放到同一個(gè)類中,修改會(huì)比較集中,代碼容易維護(hù)。實(shí)際上,我們前面講過(guò)的單一職責(zé)原則是實(shí)現(xiàn)代碼高內(nèi)聚非常有效的設(shè)計(jì)原則。

4.3 低耦合

所謂低耦合是說(shuō),在代碼中,類與類之間的依賴關(guān)系簡(jiǎn)單清晰。即使兩個(gè)類有依賴關(guān)系,一個(gè)類的代碼改動(dòng)不會(huì)或者很少導(dǎo)致依賴類的代碼改動(dòng)。實(shí)際上,我們前面講的依賴注入、接口隔離、基于接口而非實(shí)現(xiàn)編程,以及今天講的迪米特法則,都是為了實(shí)現(xiàn)代碼的低耦合。

參考資料

  1. 《設(shè)計(jì)模式之美》
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容