22 - 迪米特(LOD)原則

迪米特法則。盡管它不像 SOLID、KISS、DRY 原則那樣,人盡皆知,但它卻非常實用。利用這個原則,能夠幫我們實現代碼的“高內聚、松耦合”。本文,圍繞下面幾個問題,并結合兩個代碼實戰(zhàn)案例,來深入地學習這個法則。

  • 什么是“高內聚、松耦合”?
  • 如何利用迪米特法則來實現“高內聚、松耦合”?
  • 有哪些代碼設計是明顯違背迪米特法則的?對此又該如何重構?

何為“高內聚、松耦合”?

  • “高內聚、松耦合”是一個非常重要的設計思想,能夠有效地提高代碼的可讀性和可維護性,縮小功能改動導致的代碼改動范圍。很多設計原則都以實現代碼的“高內聚、松耦合”為目的,比如單一職責原則、基于接口而非實現編程等。
  • 實際上,“高內聚、松耦合”是一個比較通用的設計思想,可以用來指導不同粒度代碼的設計與開發(fā),比如系統(tǒng)、模塊、類,甚至是函數,也可以應用到不同的開發(fā)場景中,比如微服務、框架、組件、類庫等。本文以“類”作為這個設計思想的應用對象來展開講解,其他應用場景你可以自行類比
  • 在這個設計思想中,“高內聚”用來指導類本身的設計,“松耦合”用來指導類與類之間依賴關系的設計。不過,這兩者并非完全獨立不相干。高內聚有助于松耦合,松耦合又需要高內聚的支持。

什么是“高內聚”?

  • 所謂高內聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中,代碼容易維護??梢詤⒖?a href="http://www.itdecent.cn/p/a557f73347a8" target="_blank">單一職責原則

什么是“松耦合”?

  • 所謂松耦合是說,在代碼中,類與類之間的依賴關系簡單清晰。即使兩個類有依賴關系,一個類的代碼改動不會或者很少導致依賴類的代碼改動。實際上,我們前面講的依賴注入、接口隔離、基于接口而非實現編程,以及今天講的迪米特法則,都是為了實現代碼的松耦合。

“內聚”和“耦合”之間的關系。

  • “高內聚”有助于“松耦合”,同理,“低內聚”也會導致“緊耦合”。關于這一點,我畫了一張對比圖來解釋。圖中左邊部分的代碼結構是“高內聚、松耦合”;右邊部分正好相反,是“低內聚、緊耦合”。
高內聚,松耦合示意圖
  • 從圖中我們也可以看出,高內聚、低耦合的代碼結構更加簡單、清晰,相應地,在可維護性和可讀性上確實要好很多。

迪米特法則

  • 迪米特法則的英文翻譯是:Law of Demeter,縮寫是 LOD。單從這個名字上來看,我們完全猜不出這個原則講的是什么。不過,它還有另外一個更加達意的名字,叫作最小知識原則,英文翻譯為:The Least Knowledge Principle。

Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

每個模塊(unit)只應該了解那些與它關系密切的模塊(units: only units “closely” related to the current unit)的有限知識(knowledge)?;蛘哒f,每個模塊只和自己的朋友“說話”(talk),不和陌生人“說話”(talk)。

不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量只依賴必要的接口(也就是定義中的“有限知識”)。

理論解讀與代碼實戰(zhàn)一

  • 對于“不該有直接依賴關系的類之間,不要有依賴”。舉個例子解釋一下。
  • 這個例子實現了簡化版的搜索引擎爬取網頁的功能。代碼中包含三個主要的類。其中,NetworkTransporter 類負責底層網絡通信,根據請求獲取數據;HtmlDownloader 類用來通過 URL 獲取網頁;Document 表示網頁文檔,后續(xù)的網頁內容抽取、分詞、索引都是以此為處理對象。具體的代碼實現如下所示:
public class NetworkTransporter {
    // 省略屬性和其他方法...
    public Byte[] send(HtmlRequest htmlRequest) {
      //...
    }
}
public class HtmlDownloader {
  private NetworkTransporter transporter;//通過構造函數或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);
  }
  //...
}
  • NetworkTransporter 類:作為一個底層網絡通信類,我們希望它的功能盡可能通用,而不只是服務于下載 HTML,所以,我們不應該直接依賴太具體的發(fā)送對象 HtmlRequest。從這一點上講,NetworkTransporter 類的設計違背迪米特法則,依賴了不該有直接依賴關系的 HtmlRequest 類,重構如下:
public class NetworkTransporter {
    // 省略屬性和其他方法...
    public Byte[] send(String address, Byte[] data) {
      //...
    }
}
  • Document 類:問題比較多,主要有三點。
    • 第一,構造函數中的 downloader.downloadHtml() 邏輯復雜,耗時長,不應該放到構造函數中,會影響代碼的可測試性。代碼的可測試性我們后面會講到,這里你先知道有這回事就可以了。
    • 第二,HtmlDownloader 對象在構造函數中通過 new 來創(chuàng)建,違反了基于接口而非實現編程的設計思想,也會影響到代碼的可測試性。
    • 第三,從業(yè)務含義上來講,Document 網頁文檔沒必要依賴 HtmlDownloader 類,違背了迪米特法則。修改如下:
public class Document {
  private Html html;
  private String url;
  
  public Document(String url, Html html) {
    this.html = html;
    this.url = url;
  }
  //...
}
// 通過一個工廠方法來創(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);
  }
}

理論解讀與代碼實戰(zhàn)二

  • “有依賴關系的類之間,盡量只依賴必要的接口”。結合一個例子來講解。下面這段代碼非常簡單,Serialization 類負責對象的序列化和反序列化。
public class Serialization {
  public String serialize(Object object) {
    String serializedResult = ...;
    //...
    return serializedResult;
  }
  
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    //...
    return deserializedResult;
  }
}
  • 假設在我們的項目中,有些類只用到了序列化操作,而另一些類只用到反序列化操作。那基于迪米特法則后半部分“有依賴關系的類之間,盡量只依賴必要的接口”,只用到序列化操作的那部分類不應該依賴反序列化接口。同理,只用到反序列化操作的那部分類不應該依賴序列化接口。
  • 既不想違背高內聚的設計思想,也不想違背迪米特法則,可以通過引入兩個接口就能輕松解決這個問題:
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;
  }
  //...
}
  • 對于這個 Serialization 類來說,只包含兩個操作,其實沒有太大必要拆分成兩個接口。但是,如果我們對 Serialization 類添加更多的功能,那么拆分接口則非常適合了,工作中需要能活學活用

小結

  • “高內聚、松耦合”是一個非常重要的設計思想,能夠有效提高代碼的可讀性和可維護性,縮小功能改動導致的代碼改動范圍?!案邇染邸庇脕碇笇ь惐旧淼脑O計,“松耦合”用來指導類與類之間依賴關系的設計。
  • 所謂高內聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中。所謂松耦合指的是,在代碼中,類與類之間的依賴關系簡單清晰。即使兩個類有依賴關系,一個類的代碼改動也不會或者很少導致依賴類的代碼改動。
  • 不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量只依賴必要的接口。迪米特法則是希望減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統(tǒng)的其他部分。一旦發(fā)生變化,需要了解這一變化的類就會比較少。
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容