第二部分:理論八
“高内聚、松耦合”是一個非常重要的設計思想,能夠有效地提高代碼的可讀性和可維護性,縮小功能改動導緻的代碼改動範圍。
“高内聚”用來指導類本身的設計,“松耦合”用來指導類與類之間依賴關系的設計。
所謂高内聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中。
相近的功能往往會被同時修改,放到同一個類中,修改會比較集中,代碼容易維護。
所謂松耦合是說,在代碼中,類與類之間的依賴關系簡單清晰。
即使兩個類有依賴關系,一個類的代碼改動不會或者很少導緻依賴類的代碼改動。
文中舉例,通過兩張圖來對比。
左圖中,類的粒度比較小,每個類的職責都比較單一。相近的功能都放到了一個類中,不相近的功能被分割到了多個類中。這樣類更加獨立,代碼的内聚性更好。因為職責單一,是以每個類被依賴的類就會比較少,代碼低耦合。一個類的修改,隻會影響到一個依賴類的代碼改動。我們隻需要測試這一個依賴類是否還能正常工作就行了。
右圖中,類粒度比較大,低内聚,功能大而全,不相近的功能放到了一個類中。這就導緻很多其他類都依賴這個類。當我們修改這個類的某一個功能代碼的時候,會影響依賴它的多個類。我們需要測試這三個依賴類,是否還能正常工作。這也就是所謂的“牽一發而動全身”。
另外,高内聚、低耦合的代碼結構更加簡單、清晰,相應地,在可維護性和可讀性上确實要好很多。
迪米特法則的英文翻譯是:Law of Demeter,縮寫是LOD。
還有另外一個更加達意的名字,叫作最小知識原則,英文翻譯為:The Least Knowledge Principle。
每個子產品(unit)隻應該了解那些與它關系密切的子產品(units: only units “closely” related to the current unit)的有限知識(knowledge)。或者說,每個子產品隻和自己的朋友“說話”(talk),不和陌生人“說話”(talk)。
不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量隻依賴必要的接口(也就是定義中的“有限知識”)。
文中舉例
簡化版的搜尋引擎爬取網頁的功能,包含三個類。
NetworkTransporter 類負責底層網絡通信,根據請求擷取資料,有方法 send(HtmlRequest htmlRequest)
HtmlDownloader 類用來通過 URL 擷取網頁,有方法downloadHtml(),其中調用NetworkTransporter.send()
Document 類表示網頁文檔,後續的網頁内容抽取、分詞、索引都是以此為處理對象,構造方法中調用HtmlDownloader.downloadHtml()。
代碼示例:
問題分析
NetworkTransporter 類:
作為一個底層網絡通信類,不隻是服務于下載下傳 HTML,是以,我們不應該直接依賴太具體的發送對象 HtmlRequest。
違背迪米特法則,依賴了不該有直接依賴關系的 HtmlRequst 類。
去商店買東西不能直接把錢包給收銀員,而是把錢從錢包裡拿出來給收銀員,應該把 HtmlRequst 裡的 address 和 content 交給 NetworkTransporter。
HtmlDownloader 類:
設計沒有問題,隻需要對應修改調用NetworkTransporter.send() 的參數。
Document 類:
構造方法中邏輯過于複雜,耗時長,增加測試複雜度。
構造方法中 new 了類 HtmlDownloader,違反了基于接口而非實作程式設計。
Document 網頁文檔沒必要依賴 HtmlDownloader 類,違背了迪米特法則。
增加一個工廠類 DocumentFactory 來建立 Document,構造方法中傳入 HtmlDownloader,在方法 createDocument() 中new Document()。
NetworkTransporter 類代碼修改後:
HtmlDownloader 類代碼修改後:
Document 類代碼修改後:
Serialization 類負責對象的序列化和反序列化。
Serialization 類中有方法 serialize() 和 deserialize()。
單看這個類沒問題,但是放在一定的應用場景中,我們的項目中有些類隻用到序列化,有些類隻用到反序列化。違背迪米特法則後半部分“有依賴關系的類之間,盡量隻依賴必要的接口“。
Serialization 類代碼:
Serialization 類拆分為兩個更小粒度的類,一個隻負責序列化(Serializer 類),一個隻負責反序列化(Deserializer 類):
盡管拆分之後的代碼更能滿足迪米特法則,但卻違背了高内聚的設計思想。如果修改了序列化的實作方式,比如從 JSON 換成了 XML,那反序列化的實作邏輯也需要一并修改。通過引入兩個接口解決這個問題,具體的代碼如下所示:
迪米特法優化
拆分成兩個類,一個隻負責序列化(Serializer 類),一個隻負責反序列化(Deserializer 類)。
但是此方案違背高内聚的設計思想,相近的功能要放到同一個類中,這樣可以友善功能修改的時候,修改的地方不至于過于分散。
接口隔離優化
引入兩個接口,Serializable 和 Deserializable。
類 Serialization 實作以上兩個接口中的序列化和反序列化方法。
在調用時,雖然傳入包含序列化和反序列化的 Serialization 實作類,但是需要用到序列化就依賴 Serializable 接口,需要用到反序列化就依賴 Deserializable。
基于最小接口而非最大實作程式設計。
以上例子中,整個類隻包含序列化和反序列化兩個操作,隻用到序列化操作的使用者,即便能夠感覺到僅有的一個反序列化函數,問題也不大。是否過度設計呢?
隻包含兩個操作,确實沒有太大必要拆分成兩個接口,如果添加更多序列化和反序列化的方法,那麼拆分就很有必要。
序列化的使用者,沒有必要了解反序列化的”知識“,按照迪米特法則,将反序列化和序列化的功能隔離開來,減少耦合和測試工作量。