整潔架構、DDD 和 CQRS 簡介
介紹
在這篇博文中,我將介紹整潔架構(Clean Architecture ),它是一種現代、可擴展的正式軟件架構,適用於現代 Web 應用程序。接下來,我將討論 DDD(領域驅動設計)如何適應這幅圖景,以及 DDD 概念如何與清潔架構完美契合,從而產生一種稱爲清潔 DDD 的方法。最後,我介紹了命令查詢職責分離 (CQRS),並描述了它如何補充和增強 Clean DDD 解決方案,以創建優雅、健壯、可擴展和可測試的軟件系統。
清潔架構
清潔建築是一種相對 “現代” 的正式建築,因爲它不到十年的歷史。它隨着時間的推移從其他幾種架構演變而來,包括六邊形架構、端口和適配器以及洋蔥架構。 在這篇文章中,Bob 大叔強調了所有前身架構和清潔架構都具備的五個品質:
-
框架獨立性:架構與第三方框架解耦。
-
可測試:該架構易於編寫單元測試。
-
UI 獨立性:架構可以從用戶界面中拔出
-
數據庫獨立性:架構與底層數據存儲分離。
-
外部代理獨立性:架構的業務規則是孤立的,對外界一無所知。
Clean Architecture 可以被可視化爲一系列同心圓,每個圓代表應用程序的不同層。使架構結合在一起的原則稱爲 Dependency Rule:
“使這個架構工作的最重要的規則是依賴規則。這條規則說源代碼依賴只能指向內部。內圈中的任何東西都無法知道外圈中的某物。特別是某物的名稱內圈中的代碼不得提及外圈中聲明的內容。這包括函數、類、變量或任何其他命名的軟件實體。
這是一個很好的起點,但我想進一步詳細說明。清潔架構應用程序中的層:
-
可能永遠不知道圍繞(在)它的層。
-
也可能永遠不知道與其相鄰的層。
-
在實時應用程序中通過依賴倒置在功能上相互連接——即通過在外層實現的抽象(接口)。
整潔架構的顯着特徵是組成它的同心層圍繞着一個包含抽象和業務邏輯的中央核心。這些抽象的實現,連同它們的外部依賴,被推到外層。在描述這種佈局時,我喜歡使用 “外圍” 的術語來指代外部實現層,而 “核心” 來指代內部層。請注意,這是我的命名約定,此時您不太可能在其他任何地方找到它。
同樣,我想強調一下我們如何明確地使用依賴倒置原則來確保內部層(純邏輯和抽象)永遠不會有任何外部層(實現)的知識。內部層使用這些層中定義的抽象,而實際的實現邏輯存在於外部層中。重申一下:依賴倒置原則指出細節依賴於抽象;抽象不依賴於細節。因此,我們正在區分什麼是本質(核心)和什麼是細節(外圍)。使用依賴注入,通常通過控制容器的反轉,所有內容都在完成的解決方案中結合在一起。
整潔的領域驅動設計
我對 Clean DDD 的解讀
整潔的領域驅動設計代表了軟件架構開發的下一個合乎邏輯的步驟。這種方法源自 Blob 的原始架構,但在概念上略有不同。相同之處在於它在較高級別使用相同的同心層方法,但是使用域驅動設計來構建內核。此外,DDD 推動將域分離爲不同的有界上下文也爲這種設計提供了信息,因爲這些有界上下文現在成爲堆棧每一層水平分離的指南。這是一個真正的、現代的、以領域爲中心的模型,用於構建和交付複雜的業務應用程序。
核心層
領域層
域層是核心中最中心的層。該層是使用 DDD 原則構建的,其中沒有任何內容對它之外的任何內容有任何瞭解。在大多數情況下,這裏不使用依賴注入,儘管事件調度程序實現可能會出現罕見的異常。領域層中的領域服務和其他業務邏輯甚至不需要真正位於接口後面,因爲該邏輯不太可能隨着時間而改變,並且不需要多態性。在使用接口確實有意義的領域領域,例如使用策略模式來封裝不同的業務邏輯,繼續使用它們;否則,只需將域服務直接注入需要它們的類中。
應用層
應用層非常重要,因爲它基本上是將領域層與外層綁定的 “粘合劑”。它幾乎就像一箇中間層。應用層聲明瞭代表基礎設施、持久性和表示組件的接口和其他抽象。這些組件的實際實現不在這一層中聲明,而是通過依賴注入提供給應用程序組件。
該層還負責編排:它實現了操作域對象和啓動域工作流的高級邏輯。這樣,它本身不包含任何一流的業務邏輯,而是通過對領域層的調用來組織該邏輯。它可以協調任務並將工作委託給域,但它不包含業務規則或維護業務狀態。
應用層同樣使用注入的持久化接口執行持久化操作。這就是存儲庫模式或 CQRS 發揮作用的地方(解釋如下)。由於不同的編排操作,它將數據傳輸對象 (DTO) 傳遞到表示層。同樣,它還使用注入的基礎設施接口與操作系統和其他外部資源進行通信。
外圍層
持久層
持久層包含 應用層中聲明的持久性接口的實現。它還包含專門的持久性模型(數據訪問)類,這些類可能是也可能不是數據庫表的鏡像(特別是如果您使用對象關係映射器,又名 ORM),或者可能代表數據庫查詢的投影。這是對數據庫進行實際讀 / 寫的所有硬邏輯所在的位置。
基礎設施層
基礎設施層包含應用程序層中聲明的基礎設施接口的實現。這裏沒有太多要說的,因爲它是你所期望的:它封裝了與操作系統、外部 API 等通信的邏輯。與外部消息隊列通信的實現細節以及通信的服務都在這裏與任何其他外部機構。
表示層
表示層是一個 API 層,它彙集了所有應用程序層組件,並將它們注入適當的實現(通常使用 IOC 容器)。在我的解釋中,這一層不是用戶界面 (UI),而是呈現 UI 與之通信的外觀。在 Web 應用程序中,表示層是一個 MVC 應用程序,它使用 Web 協議(如 REST、GraphQL 或 Web 套接字)與 UI 通信。展望未來,當我談到 MVC 控制器時,要知道我總是將它們稱爲表示層組件。
現在,您需要注意一些事情。我研究過的一些資料將 Web API 視爲系統的應用層。換句話說,應用層和表示層似乎是一回事。我強烈不同意這一點。應用程序層是它自己的動物,如果需要,您應該始終能夠將其與表示邏輯分離。
用戶界面
用戶界面是該架構中絕對最高的概念層。這是用戶直接與之交互的代碼。一些示例可能是 Angular 或 React 等,它們在用戶的 Web 瀏覽器中運行,或者使用 Windows Presentation Foundation (WPF) 構建的桌面應用程序。一些消息來源將其與表示層混爲一談,但我認爲將其分開很重要,至少在 Web 應用程序中是這樣。如果架構良好,您的系統應該能夠毫不費力地移除 UI 並用不同的 UI 替換。
公共層
公共層是一個庫或一組庫,用於橫切關注點,例如日誌記錄、文本操作、日期 / 時間算法、配置等,它們對整個系統都是全局的. 公共層中的組件和接口可以在堆棧的任何層中使用(UI 除外,它可能完全斷開連接,在 Web 應用程序的情況下,完全在用戶的瀏覽器中運行)。公共層包含組件和功能的實現細節,這些細節足夠通用,可以在應用程序的任何地方使用。在這一點上,這裏絕對不應該有任何業務邏輯或與域有關的任何事情。
努力防止這一層膨脹失控。當懷疑某個東西是否屬於公共層時,想想自己,這個組件是否可以在完全不同的軟件系統中重用,甚至可以放入可重用工具包中?如果答案是 “否”,那麼您真的需要考慮它是否是一個橫切關注點,或者它是否屬於系統的另一部分。
CQRS 入門
CQRS 代表 Command/Query Responsibility Segregation,這是一件很棒的事情。這是一種架構設計模式,它允許更高級別的層(例如表示層)與其他層(例如應用層)進行通信 - 例如,表示層內的控制器將調用由應用程序執行的命令和查詢層組件。CQRS 與 Clean Domain-Driven Design 完美契合,因爲它是一種行爲模式:Clean DDD 是什麼,CQRS 是如何。
在我深入挖掘之前,我想明確一點,你不需要使用 CQRS 來實現 Clean Architecture 或 Clean DDD 解決方案,但你爲什麼不使用它呢?爲了爭論,另一種方法可能是將您的編排邏輯封裝在應用程序層服務中,這些服務直接注入您的控制器中。一切都很整潔,並尊重依賴倒置原則。但是,您已經喪失了 CQRS 提供的好處,因爲它抽象了跨層邊界的組件之間的通信過程本身。CQS 原則指出:
-
查詢系統數據的高級操作不應該產生任何副作用——即修改狀態。這些稱爲查詢。查詢通過 DTO 將數據返回到表示層。
-
修改系統的高級操作不應返回數據。這些應該會產生副作用,修改系統的狀態,然後完成。這些被稱爲命令。
-
在實踐中,命令可能會返回一小部分元數據,例如新創建的實體的 ID,但僅此而已。命令也可能返回 ack/nack 響應。
-
命令執行的另一個結果可能是錯誤條件,在這種情況下,命令應該拋出異常。
CQS 自身
CQS 可以直接在 Clean DDD 解決方案中實施。爲此,您通常會將控制器方法分解爲命令 / 查詢(即寫入 / 讀取)操作,並且永遠不會違反兩者之間的分隔。目的是創建一個基於任務的界面,它處理行爲,而不僅僅是保存數據或執行其他 CRUD 操作。請注意:這是 CQS 和 CQRS 與 DDD 相交的地方——操作本身通常會使用您正在使用的有界上下文的普遍語言以業務流程命名. 請注意,這種簡單、優雅的設計也促進了單一職責原則,因爲每個操作都更具凝聚力,並且更好地對應 UI 操作。這促進了接近用戶體驗 (UX) 驅動開發的開發過程。
CQS 的反面是我經常看到的反模式:命令和查詢之間的零分離。在這種非設計中,沒有真正的方法來理解給定操作的副作用是什麼,因爲總是有一些 Rube Goldberg 邏輯在後臺運行。這在已有 10 多年曆史的遺留應用程序中很常見,但許多應該更瞭解的職業開發人員仍然以這種方式構建解決方案。另一種常見的反模式是在控制器(Web API)上公開 CRUD 操作,然後業務邏輯分散在整個應用程序中,例如在 UI 本身中或更糟的是在存儲過程中的數據庫中。這會產生迷宮般的、脆弱的解決方案,在極其荒謬的程度上違反了開放 / 封閉原則
CQRS 是 CQS+
命令查詢職責分離,是什麼?CQRS 接受命令和查詢並將它們轉換爲一流的對象。使用 CQS、基於任務的接口的解決方案可以很容易地重構爲 CQRS,因爲邏輯分離已經存在。兩種模式的最大區別在於 CQS 中的命令 / 查詢是方法;在 CQRS 中,模型. 這裏的區別很重要。通過將操作視爲模型,您將在應用程序中引入令人難以置信的可擴展性。這與 .NET 團隊在過去引入任務並行庫 (TPL) 時所做的沒有什麼不同:他們採用了在應用程序中開發異步控制流的繁重過程,並將所有這些抽象爲一流的對象,可以獨立於依賴它們的代碼進行處理。
以下是使用 CQRS 的幾個明顯好處:
-
可擴展性:對系統的讀操作通常比寫操作多得多。在 CQRS 下,查詢可以分解成自己的堆棧並獨立於命令進行縮放。
-
性能:您可以構建在緊密耦合模型中不可能實現的優化。
-
簡單:一開始,您通過在您的架構中使用它來支付少量的複雜性,但隨着解決方案的增長以滿足業務需求,您可以在路上將其收回。隨着時間的推移,它更易於維護並且能夠更好地管理複雜性。
怎麼運行的
在較高級別上,命令 / 查詢在表示層(在控制器操作內部)中實例化並與應用層通信,然後應用層執行業務編排邏輯並執行您感興趣的高級任務。
一種方法是將命令 / 查詢參數和處理它們的邏輯都放在同一個對象中。該對象使用依賴注入注入每個控制器或使用某種工廠創建。在命令 / 查詢對象上調用 Execute() 方法並檢索結果。我不喜歡這個。
實現 CQRS 的更好方法是將命令 / 查詢與其處理程序分開,並利用進程內消息傳遞服務將命令 / 查詢對象分派給它們各自的處理程序。這樣做有很多好處,但有兩個明顯的好處是:
-
它簡化了您的代碼,因爲您不必編寫樣板來將命令 / 查詢連接到它們各自的處理程序。
-
您已經在應用程序中創建了一個高級任務執行管道,您可以在其中注入橫切關注點,例如錯誤處理、緩存、日誌記錄、驗證、重試等。這是巨大的。
命令
CQRS 命令總是以現在命令式命名——例如 RegisterEmployeeCommand。命令會改變系統狀態並返回簡單的 ack/nack 或元數據響應,或者它們會拋出異常。它們與域事件的不同之處在於可以拒絕命令;事件不能。命令通常通過應用層與域層交互。這很重要,因爲域層包含所有業務邏輯並負責使系統保持一致狀態。構成命令的屬性應儘可能接近第 3 範式 [Greg Young, CQRS Documents ]。如果你對 1NF/2NF/3NF 之間的區別感興趣,這篇 Quora 帖子很好地解釋了它. 最後,命令通常需要是冪等的。
查詢
CQRS 查詢也以現在時命名,通常以 “Get” 開頭——例如 GetEmployeeListQuery。查詢不會改變系統狀態,它們只是返回數據,通常以數據傳輸對象的形式。返回的 DTO 上的屬性的結構接近於第一範式,因爲數據可能會從非規範化的數據庫查詢中返回,並且返回的 DTO 的結構通常會匹配用戶的屏幕或某些可由用戶使用的規範模型任何客戶。
在他的原始規範中,我研究過的大多數專家都同意這一點,Greg Young 指出大多數時候查詢應該繞過域層。讓我們進一步解開它。爲什麼我們要直接從應用層傳遞到表示層?首先,數據模型比用戶輸入更可靠,並且假設始終是一致的,因此不需要進行驗證的數據庫。其次,查詢不會改變狀態,因此進行這種操作的業務域邏輯沒有用處。第三,在系統中讀取(查詢)將比寫入(命令)更頻繁地發生,因此它們需要快速高效。
命令和查詢的其他注意事項:
-
如 CQS 部分所述,命令和查詢都應使用通用語言命名,並表示基於任務的操作,而不是 CRUD。
-
後綴 “命令” 和“查詢”是可選的,因此請自行決定。只需確保您的命名約定直觀且一致。
-
“可選” 屬性是一種設計味道,可能表明您的基於任務的操作不夠內聚。
-
不要從其他命令 / 查詢調用命令 / 查詢。如果你這樣做,這是一個很大的氣味。如果您需要在命令邏輯中從數據庫中檢索數據,那麼您應該簡單地使用 ORM 或其他方法直接查詢數據庫。請注意,這是一個典型的例子,其中必須違反某些原則才能維護其他原則。在這種情況下,我們違反了 DRY,以避免危險地將堆棧的兩側耦合在一起並將整個事物變成無法維護的垃圾堆。如果您發現自己在鬆散耦合和 DRY 之間爭論不休,那麼鬆散耦合會勝出。
整潔 DDD + CQRS
一切都導致了這一點。展望未來,我將使用它作爲 Web 應用程序開發的主要架構方法,這也是我在演示應用程序中使用的方法。重申一下,高級架構基於清潔架構原則,在系統的同心層之間具有明確的概念分離。
-
系統的最內層,核心的中心,是 Domain 層,它是使用 DDD 原則構建的。應用層圍繞着領域層,是核心的一部分。應用層包含命令 / 查詢內部的業務編排邏輯、由外圍層實現的接口以及用於與外部層通信的模型類。
-
表示層、技術實現層(基礎設施層)和 持久層位於外圍,彼此之間沒有明確的瞭解。表示層本質上是一個 Web API,一些任意 UI,如 Angular,可以與之通信。
-
CQRS 是允許各層在堆棧中優雅地進行通信的基本要素。這就是將一切聯繫在一起的原因。依賴注入同樣對於將組件連接在一起至關重要,同時仍然遵守依賴倒置原則。IOC 容器對此有所幫助。
再一次,沒有靈丹妙藥,那麼有哪些利弊呢?
優點
-
如果我們構建好我們的抽象,那麼這個架構就獨立於外部框架、用戶界面、數據庫等。換句話說,它是靈活的。框架和外部資源可以更輕鬆地插入 / 拔出。
-
該解決方案更具可測試性。
-
它更具可擴展性。
-
更容易管理必要的複雜性,即由於試圖解決實際業務問題而引入的複雜性。
-
該解決方案可以由不同的團隊進行工作和維護,而不會互相影響。Clean DDD 非常適合敏捷流程。
-
添加新功能(包括複雜功能)要簡單得多,從而使開發人員能夠更快地進行調整並更快地發佈。隨着系統隨着時間的推移而增長,添加新功能的難度保持不變且相對較小。這與我工作過的許多遺留系統形成鮮明對比,在這些遺留系統中,隨着時間的推移,添加新功能變得更加昂貴和困難,直到最終整個事情越過了可維護性的懸崖並且必須報廢。
-
如果解決方案沿有界上下文線正確分解,則將其部分轉換爲微服務變得很容易。
-
以上所有觀點都指向同一個結論,即整體系統將具有更長的壽命,並且從長期來看成本更低。
缺點
-
這是一個複雜的架構,需要對質量軟件原則(例如 SOLID、架構級別的解耦等)有深刻的理解。實施此類解決方案的任何團隊幾乎肯定需要專家 (YOU) 來推動解決方案並使其遠離發展錯誤的方式並積累技術債務。
-
清潔 DDD 需要更多的儀式,而不僅僅是編寫由單個項目組成的簡單的單片 3 層應用程序。
-
增加了支持架構的前期複雜性,這可能需要使用額外的工具包來最大程度地減少痛苦,例如 AutoMapper 和 MediatR。
-
這種架構通常不適合簡單的 CRUD 應用程序,並且可能會使此類解決方案過於複雜。相反,可以說 CRUD 是不自然的,違揹人性,並迫使業務團隊改變他們的行爲以適應軟件系統,而不是相反。但這是一個不同的討論...
-
改變(如改變主意)是困難的。試圖獲得管理層和其他團隊成員的支持可能需要大量的說服力。
-
總之,這種方法的前期成本更高。你需要決定在你的情況下是否值得。
高級主題
首先,命令與查詢的分離允許您將模塊一直拆分到數據庫。在極端架構中,可能有一個僅用於命令的數據庫和一個或多個僅用於讀取的單獨數據庫。此外,可以對讀取的數據庫進行非規範化,這可以極大地提高性能和可伸縮性。我目前無意在演示應用程序中實現這種架構。
CQRS 的極端邏輯結論導致了一種稱爲事件溯源的架構模式,這本質上意味着狀態數據不存儲在命令數據庫中,而是一系列事件,這些事件使數據從一些基本的初始化狀態發生了變異。通過 “重放” 事件,可以獲得數據的快照,這使您可以從任何時間點獲取數據的狀態。此快照可以通過最終一致性隨時間同步到讀取數據庫,或其他一些複製模式。這種方法可能適用於某些高級應用程序,例如金融應用程序。出於我們的目的,我可以擁有一個單一的數據庫而不是嘗試實現事件溯源。您爲實現這種高級架構模式付出的代價是顯着增加了複雜性。最後,我研究過的大多數專家都同意 CQRS 可以在不使用事件溯源的情況下提供巨大的好處。這是我建議您謹慎行事的另一個領域,因爲這些高級模式不適合膽小的人。
結論
在這篇博客文章中,我介紹了 Clean Architecture,它是一種一流的架構,它隨着時間的推移從其他幾種架構方法發展而來,並首先由 Bob 正式化。然後我討論了領域驅動設計如何與 Clean Architecture 結合以產生 Clean DDD,這是一種架構方法,它將 DDD 的方法論和以業務爲中心與 Clean Architecture 的邏輯分離相結合,以產生優雅且更容易轉換爲微服務的應用程序. 最後,我介紹了 CQRS,這是一種行爲架構模式,它增強了 Clean DDD,從提高性能到更輕鬆的測試和更好的可擴展性,一切都得到了改善。
來源: https://www.toutiao.com/a7057052695844274727/?log_from=9f3c53df832bb_1645595473806
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2zUk0e-PoG9jSzELy7FjCA