迪米特法則。盡管它不像 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ā)生變化,需要了解這一變化的類就會比較少。