天天看點

第二部分:理論八

第二部分:理論八

“高内聚、松耦合”是一個非常重要的設計思想,能夠有效地提高代碼的可讀性和可維護性,縮小功能改動導緻的代碼改動範圍。

“高内聚”用來指導類本身的設計,“松耦合”用來指導類與類之間依賴關系的設計。

所謂高内聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中。

相近的功能往往會被同時修改,放到同一個類中,修改會比較集中,代碼容易維護。

所謂松耦合是說,在代碼中,類與類之間的依賴關系簡單清晰。

即使兩個類有依賴關系,一個類的代碼改動不會或者很少導緻依賴類的代碼改動。

文中舉例,通過兩張圖來對比。

左圖中,類的粒度比較小,每個類的職責都比較單一。相近的功能都放到了一個類中,不相近的功能被分割到了多個類中。這樣類更加獨立,代碼的内聚性更好。因為職責單一,是以每個類被依賴的類就會比較少,代碼低耦合。一個類的修改,隻會影響到一個依賴類的代碼改動。我們隻需要測試這一個依賴類是否還能正常工作就行了。

右圖中,類粒度比較大,低内聚,功能大而全,不相近的功能放到了一個類中。這就導緻很多其他類都依賴這個類。當我們修改這個類的某一個功能代碼的時候,會影響依賴它的多個類。我們需要測試這三個依賴類,是否還能正常工作。這也就是所謂的“牽一發而動全身”。

另外,高内聚、低耦合的代碼結構更加簡單、清晰,相應地,在可維護性和可讀性上确實要好很多。

迪米特法則的英文翻譯是: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。

基于最小接口而非最大實作程式設計。

以上例子中,整個類隻包含序列化和反序列化兩個操作,隻用到序列化操作的使用者,即便能夠感覺到僅有的一個反序列化函數,問題也不大。是否過度設計呢?

隻包含兩個操作,确實沒有太大必要拆分成兩個接口,如果添加更多序列化和反序列化的方法,那麼拆分就很有必要。

序列化的使用者,沒有必要了解反序列化的”知識“,按照迪米特法則,将反序列化和序列化的功能隔離開來,減少耦合和測試工作量。