【架構設計】如何讓你的應用做到高內聚、低耦合?

2023-01-06 15:00:33

前言

最近review公司的程式碼,發現程式碼耦合程度特別高,修改一處,不知不覺就把其他地方影響到了,這就讓我思考該如何讓我們寫的程式碼足夠內聚,減少耦合呢?

"高內聚、鬆耦合"是一個非常重要的設計思想,能夠有效地提高程式碼的可讀性和可維護性,縮小功能改動導致的程式碼改動範圍。它可以用來指導不同粒度程式碼的設計與開發,比如系統、模組、類,甚至是函數,也可以應用到不同的開發場景中,比如微服務、框架、元件、類庫等。本文我們來探討下如何讓我們的應用做到高內聚、低耦合。

歡迎關注個人公眾號『JAVA旭陽』交流溝通

什麼是高內聚?

首先我們將目光投射到內聚上,通常我們的程式碼的內聚歸為7類,如下圖所示,內聚性從高到低。

  1. 功能內聚

將相同功能放到一個類或者模組中,內聚程度最高。

  1. 順序內聚

如果一個功能的輸出是另外一個功能的輸入,這種存在順序依賴關係的,我們將他們歸到一個模組中叫做順序內聚。

  1. 通訊內聚

如果功能點使用相同輸入或輸出資料,我們將他們內聚到一個模組中,叫做通訊內聚。

  1. 過程內聚

如果不同的功能是由同一個控制流支配的,我們稱作過程內聚。

  1. 時間內聚

不同的功能在同一時間段內執行,比如銀行不同的跑批任務,由時間去控制是否放在同一個模組中的內聚叫做時間內聚。

  1. 邏輯內聚

不同的功能可能他們內部的邏輯是一致的,我們將他歸在一起叫做邏輯內聚。

  1. 偶然內聚

而偶然內聚是指不同的功能,沒什麼關聯就直接放在一起了。這種情況很常見,比如A團隊同時承擔了支付、物流、產品等功能的開發,為了節省開發資源,他們就把不相干的功能放到一個模組中,那麼這種偶然內聚會隨著業務發展遇到各種問題。

上面講解了內聚的可能7種分類,大家不用太較真,瞭解一下就行。

小結一下,我們所說的高內聚,其實是用來指導類或者模組本身的設計,簡單來說,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中,程式碼容易維護。

什麼是低耦合?

現在我們聚焦到耦合上,耦合實際上關注在類與類之間或者模組與模組之間依賴關係的設計,我們通常有下面7種型別的耦合關係。

  1. 非直接耦合

兩個模組之間沒有直接關係,模組獨立性最強

  1. 資料耦合

兩個模組之間用引數傳遞關聯,模組之間影響最小的耦合關係。比如訂單系統輸入物流編號ID,物流系統返回你具體的物流資訊。

  1. 標記耦合

兩個模組依賴同樣的一個資料結構,傳遞的是資料結構。

比如租房費用計算系統,需要計算水費和電費,如果你傳使用者資訊這個資料結構,讓水費和電費系統領取使用者物件的用水量和用電量,這就是標記耦合。更好的做法應該只需要傳必要的用水量和用電量,而不是傳輸整個使用者物件。

  1. 控制耦合

兩個模組之間傳輸控制資訊,比如某個標誌或者開關,呼叫模組需要知道被呼叫模組的內部邏輯,增加了相互依賴和理解的複雜度。

  1. 外部耦合

一組模組需要與外部環境關聯,這組模組存取同一全域性變數,外部耦合有時候必不可少,但應儘量減少此類模組數量。

  1. 公共耦合

一組模組均存取同一全域性資料區,這種情況叫做公共耦合。比如有一個公共變數,不同模組都去修改它。

  1. 內容耦合

一個模組直接操作或修改另外一個模組的內部書,一個模組不通過正常入口存取另外一個模組,這就是內容耦合,是最糟糕的情況,需要避免。比如直接呼叫另外一個類的set方法,所以這就要求我們不要無腦的把類的任何屬性都加上set方法。

上面大致分享了耦合的7種情況,你們專案屬於哪種情況呢?

這邊再總結一下, 所謂低耦合是說,在程式碼中,類與類之間的依賴關係簡單清晰。即使兩個類有依賴關係,一個類的程式碼改動不會或者很少導致依賴類的程式碼改動。

高內聚,低耦合有什麼關係?

前面講解了內聚和耦合的多種情況,那麼高內聚、低耦合之間有什麼關係呢?

上圖中左邊部分的程式碼設計中,類的粒度比較小,每個類的職責都比較單一。相近的功能都放到了一個類中,不相近的功能被分割到了多個類中。這樣類更加獨立,程式碼的內聚性更好。一個類的修改,只會影響到一個依賴類的程式碼改動。我們只需要測試這一個依賴類是否還能正常工作就行了。

上圖中右邊部分的程式碼設計中,類粒度比較大,低內聚,功能大而全,不相近的功能放到了一個類中。這就導致很多其他類都依賴這個類。當我們修改這個類的某一個功能程式碼的時候,會影響依賴它的多個類。我們需要測試這三個依賴類,是否還能正常工作。

所以說, 「高內聚」有助於「鬆耦合」,同理,「低內聚」也會導致「緊耦合」

如何做到高內聚、低耦合呢?

關於如何做到模組或者類的高內聚、低耦合,有一個很經典的指導法則,那就是迪米特法則

迪米特法則是說不該有直接依賴關係的類之間,不要有依賴;有依賴關係的類之間,儘量只依賴必要的介面。換更形象點的說,每個模組只和自己的朋友「說話」(talk),不和陌生人「說話」(talk)。

展開來講,我們在做設計的時候特別關注下面的一些要點。

  1. 多用介面隱藏實現的細節, 介面是一個契約,相對穩定。
  2. 儘量避免不同模組或者類之間共用全域性變數,萬一一個地方需要修改,連帶的其他模組也會影響
  3. 能用private的地方,堅決不用public, 不要給類的任何屬性都加set方法,體現良好的封裝性,暴露越少,影響也就越少。
  4. 合理使用設計模式,因為設計模式會很好的保證程式碼的可延伸性、封裝性
  5. 注意分層設計和呼叫,比如在業務層直接用SQL語句運算元據庫,而應該講資料庫操作封裝到DAO層中,業務層呼叫DAO層
  6. 避免直接操作或者呼叫其他模組或類(內容耦合),也就是直接呼叫set方法,而是應該通過介面呼叫
  7. 儘量使用資料耦合,少用控制耦合。比如傳輸一個普通的資料變數是合適的。但傳輸一個控制命令給你,讓你去執行分支a還是分支b還是分支c,這就不是很合適,所以儘量採用一些其他的方法來規避掉這些控制命令的傳送。
  8. 模組的功能劃分儘可能的單一, 遵循單一職責原則
  9. 模組只堆外暴露最小限度的介面,遵循介面隔離原則
  10. 模組之間的互動要儘量少,介面的設計要儘量簡單,比如能傳基本類型別就不要傳輸整個物件。

總結

「高內聚、鬆耦合」是一個非常重要的設計思想,能夠有效提高程式碼的可讀性和可維護性,縮小功能改動導致的程式碼改動範圍。「高內聚」用來指導類本身的設計,「鬆耦合」用來指導類與類之間依賴關係的設計。

所謂高內聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中。所謂鬆耦合指的是,在程式碼中,類與類之間的依賴關係簡單清晰。即使兩個類有依賴關係,一個類的程式碼改動也不會或者很少導致依賴類的程式碼改動。

所以我們在設計的時候,儘量要多動動腦子,想想這個功能是不是屬於這個模組的呢?他們之間的互動合理嗎?複雜嗎?有沒有更簡單的方式呢?多問自己幾個問題,你做出的設計會越來越優秀。

歡迎關注個人公眾號『JAVA旭陽』交流溝通