如何利用迪米特法則實現(xiàn)“高內(nèi)聚、低耦合”?
1.何為“高內(nèi)聚、低耦合”?
“高內(nèi)聚、低耦合”能有效地提高代碼可讀性、可維護性,縮小功能改動導(dǎo)致的代碼改動范圍。很多設(shè)計原則也都以實現(xiàn)代碼“高內(nèi)聚、低耦合”為目的,比如:
- 單一職責(zé)原則
- 面向接口,而非面向?qū)崿F(xiàn)來編程
“高內(nèi)聚、低耦合”是個通用設(shè)計思想,可指導(dǎo):
- 不同粒度代碼的設(shè)計與開發(fā)
如系統(tǒng)、模塊、類,甚至函數(shù)
- 不同開發(fā)場景
- 如微服務(wù)、框架、組件、類庫等
本文主要圍繞以“類”作為該設(shè)計思想的應(yīng)用對象。
- “高內(nèi)聚”,指導(dǎo)類本身的設(shè)計
- “低耦合”,指導(dǎo)類與類之間依賴關(guān)系的設(shè)計
二者并非完全獨立,高內(nèi)聚有助于低耦合,低耦合又需要高內(nèi)聚的支持。
1.1 高內(nèi)聚
- 相近的功能,應(yīng)放到同一個類
- 不相近的功能,不要放到同一個類
相近的功能往往會被同時修改,放到同一類中,修改會比較集中,代碼易維護。單一職責(zé)原則就是實現(xiàn)代碼高內(nèi)聚非常有效的設(shè)計原則。
1.2 低耦合
在代碼中,類與類之間的依賴關(guān)系簡單清晰。
即使兩個類有依賴關(guān)系,一個類的代碼改動不會或很少會導(dǎo)致依賴類的代碼改動。依賴注入、接口隔離、面向接口編程及迪米特法則都是為實現(xiàn)低耦合。
1.3 “內(nèi)聚”和“耦合”的關(guān)系
左邊代碼結(jié)構(gòu)是“高內(nèi)聚、低耦合”;右邊“低內(nèi)聚、緊耦合”:
左邊的代碼設(shè)計:
類的粒度較小,每個類的職責(zé)都比較單一。相近功能都放到了一個類,不相近功能分割到多個類。這樣類更加獨立,代碼內(nèi)聚性更好。
因為職責(zé)單一,所以每個類被依賴的類就會比較少,代碼低耦合。一個類的修改,只會影響到一個依賴類的代碼改動。只需要測試這一個依賴類是否還能正常工作即可。
右邊代碼設(shè)計:
類粒度比較大,低內(nèi)聚,功能大而全,不相近的功能放到了一個類中。導(dǎo)致很多其他類都依賴該類。修改這個類的某功能代碼時,會影響依賴它的多個類。我們需要測試這三個依賴類,是否還能正常工作,“牽一發(fā)而動全身”。
2.迪米特法則
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.
每個模塊都只應(yīng)該了解那些與它關(guān)系密切的模塊(units: only units “closely” related to the current unit)的有限知識(knowledge)?;蛘哒f,每個模塊只和自己的朋友“說話”(talk),不和陌生人“說話”(tal?k)。
結(jié)合實際,定義描述中的“模塊”替換成“類”:
- 不該有直接依賴關(guān)系的類之間,不要有依賴
- 有依賴關(guān)系的類之間,盡量只依賴必要的接口(“有限知識”)
所以,迪米特法則其實包含兩部分,下面用兩個案例分別解讀。
3.案例
3.1 不該有直接依賴關(guān)系的類之間,不要有依賴
簡化的搜索引擎爬取網(wǎng)頁,包含如下主要類:
NetworkTransporter,負責(zé)底層網(wǎng)絡(luò)通信,根據(jù)請求獲取數(shù)據(jù):
HtmlDownloader,通過URL獲取網(wǎng)頁:
Document,表示網(wǎng)頁文檔,后續(xù)的網(wǎng)頁內(nèi)容抽取、分詞、索引都是以此為處理對象:
如何重構(gòu)?
這段代碼雖然“能用”,但不夠“好用”。
NetworkTransporter,作為一個底層網(wǎng)絡(luò)通信類,應(yīng)該盡可能通用,而不是只能下載HTML。所以,不應(yīng)該直接依賴太具體的發(fā)送對象HtmlRequest,其設(shè)計違背迪米特法則,依賴了不該有直接依賴關(guān)系的HtmlRequest類。
假如你現(xiàn)在要去買東西,你肯定不會直接把錢包給收銀員,讓收銀員自己從里面拿錢,而是你從錢包里把錢拿出來交給收銀員。HtmlRequest對象相當(dāng)于錢包,HtmlRequest里的address和content對象就相當(dāng)于錢。
應(yīng)將address和content交給NetworkTransporter,而非直接把HtmlRequest交給NetworkTransporter:
Document的問題:
- 構(gòu)造器中的downloader.downloadHtml()邏輯復(fù)雜,耗時長,不應(yīng)放到構(gòu)造器,影響代碼可測試性
- HtmlDownloader對象在構(gòu)造器通過new來創(chuàng)建,違反面向接口編程,也影響代碼可測試性
- 業(yè)務(wù)上說,Document網(wǎng)頁文檔沒必要依賴HtmlDownloader類,違背迪米特法則
問題雖多,但修改簡單:
3.2 有依賴關(guān)系的類之間,盡量只依賴必要的接口。
Serialization類負責(zé)對象的序列化和反序列化:
單看類的設(shè)計,沒問題。但若放到特定應(yīng)用場景,假設(shè)項目中的有些類只用到序列化操作,而另一些類只用到反序列化。那么,基于 有依賴關(guān)系的類之間,盡量只依賴必要的接口:
- 只用到序列化操作的那部分類不應(yīng)依賴反序列化接口
- 只用到反序列化操作的那部分類不應(yīng)依賴序列化接口
據(jù)此,應(yīng)將Serialization類
方案一:拆分為兩個更小粒度的類
- 只負責(zé)序列化(Serializer類)
- 只負責(zé)反序列化(Deserializer類)
盡管拆分后的代碼更能滿足迪米特法則,但卻違背高內(nèi)聚。高內(nèi)聚要求相近功能放到同一類中,方便功能修改時,修改的地方不太散亂。
針對本案例,若業(yè)務(wù)要求修改序列化實現(xiàn)方式,從JSON換成XML,則反序列化實現(xiàn)邏輯也要一起改。未拆分前,只需修改一個類,拆分后,卻要修改兩個類!
既不想違背高內(nèi)聚,也不想違背迪米特法則,怎么辦?
方案二:引入兩個接口
盡管還是要往Demo1的構(gòu)造器傳入包含序列化和反序列化的Serialization實現(xiàn)類,但依賴的Serializable接口只包含序列化操作,Demo1無法使用Serialization類中的反序列化接口,對反序列化操作無感知,符合迪米特法則的“依賴有限接口”。
也體現(xiàn)了“面向接口編程”,結(jié)合迪米特法則,可總結(jié)出:“基于最小接口,而非最大實現(xiàn)來編程”。
多想一點點
本案例的重構(gòu)方案,整個類只包含序列化、反序列化倆操作,只用到序列化操作的使用者,即便能夠感知到僅有的一個反序列化方法,問題也不大。為滿足迪米特法則,將一個簡單的類,拆出兩個接口,是過度設(shè)計嗎?設(shè)計原則本身無對錯,只有能否用對之說。不要為了用設(shè)計原則而用,應(yīng)該具體問題具體分析。
Serialization類只包含兩個操作,確實沒啥必要拆成倆接口。但若對Serialization類添加更多功能,實現(xiàn)更多更好用的序列化、反序列化方法,重新考慮該問題:
這種場景下,方案二設(shè)計更好。因為本案例的應(yīng)用場景,大部分代碼只用到序列化功能,這些用戶無需了解反序列化,而修改后的Serialization類,反序列化的“知識”,從一個方法變成三個。一旦任一反序列化操作有代碼改動,都需要檢查、測試所有依賴Serialization類的代碼是否還能正常工作。
為減少耦合和降低測試的工作量,應(yīng)按迪米特法則,隔離反序列化和序列化的功能。
4.總結(jié)
4.1 高內(nèi)聚、低耦合
能有效提高代碼的可讀性和可維護性,縮小功能改動導(dǎo)致的代碼改動范圍:
- 高內(nèi)聚,指導(dǎo)類本身的設(shè)計
就是指相近的功能應(yīng)該放到同一個類中,不相近的功能不要放到同一類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中
- 低耦合,指導(dǎo)類與類之間依賴關(guān)系的設(shè)計
在代碼中,類與類之間的依賴關(guān)系簡單清晰。即使兩個類有依賴關(guān)系,一個類的代碼改動也不會或者很少導(dǎo)致依賴類的代碼改動。
4.2 迪米特法則
不該有直接依賴關(guān)系的類之間,不要有依賴;有依賴關(guān)系的類之間,盡量只依賴必要的接口。迪米特法則希望減少類之間的耦合,讓類越獨立越好。每個類都應(yīng)該少了解系統(tǒng)的其他部分。一旦發(fā)生變化,需要了解這一變化的類就會比較少。
參考:
[1]. 《重構(gòu)》
[2]. 《代碼設(shè)計之丑》