設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象 - Jesse

banq發表於2021-08-10

本文將物件導向分析設計的單一職責等SOLID原則應用於微服務劃分,以及DDD領域劃分/上下文分界/DDD聚合等設計概念中,是一種實際中每天重複的設計習慣:

松耦合高內聚這兩個術語似乎同時存在的:這兩個概念是一起創造的,如果您談論其中一個,通常也會出現另一個。
類似地,DRY(不要重複自己)和錯誤抽象的概念也是同時存在的:例如,一個人說我們應該幹掉這段程式碼,另一個人說他們考慮過,但他們不想建立錯誤的抽象.
但是,我很少在同一個對話中聽到這兩組概念,這讓我感到驚訝,因為它們實際上是在談論同一件事。
請允許我解釋一下。這是一個盒子:

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
它定義了您有權更改的系統邊界。你可以改變的東西放在盒子裡。盒子裡面的東西需要改變的唯一原因是為了外面的東西:因為外部系統發生了變化,使用者的新需求,或者你試圖建模的領域的變化。
例如,假設我們有一個foo()渲染frame的函式,使用者體驗團隊每隔一週就會決定這個frame需要更改為某種新顏色。實際上,我們有一個指向foo(),這是依賴我們設計團隊的依賴項,因為當設計團隊對foo()改變時,這裡也必須改變。我們將這些輸出依賴箭頭塗成綠色。

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
好的,我們有我們的作品。讓我們用它們來表示松耦合
 

松耦合

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
透過完全沒有耦合,我們讓這一切變得容易:也就是說,我們有兩個模組,每個模組都包含一些服務,而這些服務因完全不同的原因而發生變化。財務模組只是為了財務團隊而改變,圖片下載器是客戶使用的。我們的兩個服務在域中是不相關的,在我們的程式碼中是獨立的,並且被分成不同的模組。
如果將我們的兩個服務移到一個模組中會怎樣?這將使我們從松耦合低內聚了

低內聚

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
它們將繼續單獨發展,因為它們在域中和在我們的程式碼中仍然是獨立的,但是由於其中存在兩個概念上不相關的服務,開發人員將更難從邏輯上推理這個模組。如果該模組是我們需要獨立部署的包,那麼我們現在將在對影像下載器進行更改時重新部署財務服務,反之亦然,從而導致不必要的部署。
 

內聚 + 非DRY
如果我們兩件事情就是出於同樣的原因需要改變?考慮我們有兩個函式的類的情況,foo()和bar(),這兩個函式共用一個需要前後進行更新的重疊片。我們認為這個類是內聚的,因為函式緊密相關,但我們不會稱它為DRY,因為我們在重複程式碼。

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
我們可以透過分解出一個通用baz()函式來解決這個DRY問題,我們得到了一個DRY高內聚的結果,不會出錯,就像我們最初的松耦合示例一樣。
 

內聚 + DRY

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
(藍色箭頭表示系統的依賴項)
 

共同點是什麼?
我們從一個好的狀態開始,一次調整一件事,最終進入另一個好的狀態。請注意,我們改變的每一步都不同:

  1. 在第一步中,我們透過將金融服務和影像服務移到同一個模組中來改變託管程度(我們的程式碼有多接近,集中託管高表示程式碼很接近)。
  2. 在第二步中,我們透過考慮兩個程式碼段出於相同原因需要更改的示例來更改域相互依賴的程度(綠色箭頭)
  3. 在第三步中,我們透過提取一個公共函式並在程式碼中為該函式新增幾個依賴項來改變實際相互依賴的程度(藍色箭頭)。

我認為這三步:託管領域相互依存實際相互依存,構成了涵蓋耦合內聚DRY錯誤抽象以及其他一些事情的基礎。
鑑於我們的每步驟相互獨立,我們最終得到 8 (2^3) 個排列,其中四個我們已經在上面展示過。看看剩下的四個你眼熟否?
我們上面最後一個例子是高度域相互依賴、高度實際相互依賴和高度託管。現在讓我們切換到低域相互依賴:
 

錯誤的抽象

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
(紅色箭頭也表達了實際的相互依賴,但顏色反映了它們引起的痛苦)
這為我們提供了經典的錯誤抽象示例:我們有一個函式嘗試做所有事情,其中​​包含處理兩個單獨用例的程式碼,每個用例都有不同的更改原因(由兩個單獨的輸出依賴項表示)。解決這個問題的辦法是拆除抽象,分離用例(即因為領域相互依賴程度低,所以應該是實際的相互依賴和託管)。
 

緊耦合
對於下一個示例,我們將切換到低集中託管。這實際上是我在工作中遇到的一個問題:我們有一個node app,它依賴於一個包,其中包含 node app實際使用的程式碼(標記為 A)以及一些特定於瀏覽器的程式碼(標記為 B) . 我們無意中向 B 新增了一些程式碼,如果沒有瀏覽器存在,無論我們是否明確將其匯入到我們的Node應用程式中,都會包含該程式碼!

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
解決方案是將 A 移出包並進入我們的node app(在我們的例子中沒有其他程式碼依賴於它)。
減少集中託管會使問題呈指數級複雜化,因為您現在不是直接依賴於幾個程式碼塊,而是依賴於包含這些程式碼塊的整個模組/包。與我們的node app一樣,有時該模組/包中的額外內容會使您的伺服器崩潰。
 

註定要在一起的微服務
對於這個例子,我們將域相互依賴設定為高:

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
這裡我們有兩個總是需要同時更改的微服務,這意味著與前面的示例不同,我們永遠不會單獨更新一個服務。這通常意味著對於每次更改,A 需要向 B 傳遞一些不同的引數,或者呼叫一個新的端點。這個問題的解決方案就是將兩個微服務合二為一!稍微少一點,但仍然提供服務。
這證明了將您的集中託管與域的相互依賴性相匹配的重要性:如果領域中確定兩件事總是同時發生變化,則它們越接近越好。透過共享部署,不僅在詞彙上更接近,而且在物理上更接近。
 

危險的重複程式碼
我們現在已經研究了七種排列,這意味著我們已經到了最後一種排列。為此,我們將保持低集中託管和高域相互依賴,但刪除實際的相互依賴(即憤怒的紅色箭頭)。典型的例子是在完全獨立的模組中有兩個重複的函式,我們希望在其中保持函式同步。

設計習慣比較:高凝聚/松耦合、DRY/錯誤抽象  - Jesse
鑑於編譯器不知道我們想要保持函式同步,開發人員在更新方法時使用他的心靈感應本能來搜尋整個程式碼庫以查詢潛在的重複函式,以防萬一也應該更新。這裡顯而易見的解決方案是刪除重複的函式並將其所有呼叫者重定向到原始函式。如果我們有相同的重複但在單個檔案中(即更高的託管),這不會有什麼大不了的,因為更容易發現相似之處,但是隨著您將集中託管從相同檔案分離到相同模組,問題變得越來越有害。
 

結論
遍歷這個 2x2x2 組合難題後,我們能學到什麼?在每個示例中,解決方案始終是將我們實際的相互依賴和集中託管設定為我們的領域相互依賴。也就是說,如果兩段程式碼因為完全不同的原因發生變化,你不僅應該將它們分開,還要儘量減少它們之間程式碼的依賴關係。相反,如果兩段程式碼因為完全相同的原因發生變化,你不僅應該將它們靠近在一起,還應該透過程式碼中的相互依賴來表示它們在域中的相互依賴,無論是透過共享一些公共介面,相互呼叫,還是分解通用程式碼。

DRY原則和錯誤抽象都很在乎領域的相互依存和相互依存的現實,但不是很關注是否集中託管。
緊耦合關心實際的相互依賴,但只在低集中託管時。
而內聚關心領域相互依賴,但只在集中託管很高時。
這些不同的概念涵蓋了很多領域,但不足以捕捉由其底層軸產生的所有情況。希望這篇文章為您提供了一個模式,當您在野外遇到這些依賴困境時,可以透過這些困境進行推理。

相關文章