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)代碼的低耦合。
參考資料
- 《設(shè)計(jì)模式之美》