日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

對領域驅動設計中關鍵的一些概念,大家有了更為深入的認識是不夠的,在具體實踐中我們還會面臨諸如代碼如何分層、不同上下文之間如何集成,以及某些時候還會用到CQRS。本文就來補齊領域驅動設計中剩余的一些內容,希望能夠助你更游刃有余地應對開發中遇到的各種問題。

作者 | 于振

責編 | 韓楠

你好,今天我想與你聊聊DDD中的應用架構。在過往我分享的幾篇文章中,我們介紹了領域驅動設計中的一些基本概念,這里,再做一個簡單的回顧。

·《基礎問題不簡單|怎么合理使用值對象,讓你的代碼更清晰、更安全?》

·《不想只做Cruder?實體、聚合根,還不快去了解下》

·《如何通過倉儲,對實體進行持久化處理?》

·《實體表達力不夠?那你應該試試領域服務》

·《如何使用工廠,進一步解耦領域對象的職責》

·《領域模型細節太多不便使用?那就加個應用服務吧》

·《DDD在Go中如何落地|如何在業務中使用領域事件?》

使用值對象和實體幫助我們構建了具有豐富行為的領域模型,實體創建出來后需要通過倉儲進行持久化,如果領域模型跟數據模型存在差異,就還需要通過 Converter 進行轉換,以及通過 Snapshot 對實體進行追蹤。

如果某些行為不適合放到某個實體上,就需要使用領域服務,同時,為了一定程度地防止領域服務的濫用,我們規定領域服務在命名上必須有一個動詞。

為了解耦領域對象的創建過程和其自身行為,我們又介紹了工廠方法。

對于外部用戶來說,領域之內的各個對象描述的,都是細粒度的領域概念,為了方便外部調用,同時屏蔽領域對象的具體細節,就又有了應用服務。

最后,通過領域事件,進一步解耦了不同上下文之間的依賴,即使在同一邊界之內的不同的聚合根,也可以實現數據的最終一致性。

至此,大家應該對領域驅動設計中關鍵的一些概念,有了更為深入的認識。但僅僅是這些應該是還不夠的,在具體實踐中,我們還面臨著諸如代碼如何分層、不同上下文之間如何集成,以及某些時候還會用到CQRS。

在這篇文章中,我們就來補齊領域驅動設計中剩余的一些內容。

首先,我們從代碼的分層開始說起。

01? DDD的分層架構

分層架構作為一種歷史悠久的架構模式,在很多的場景中都得到了應用。

大家比較熟悉的應該就是 MVC 對應用三層架構的拆分。MVC 這種分層是自上而下的。

隨著業務越來越復雜,人們逐漸發現, MVC 架構在應對復雜的業務問題時會顯得力不從心。

于是,后面逐漸演化出了六邊形架構、洋蔥架構、整潔架構等架構模式。這幾種架構也是一種分層架構,但這種分層不是由上而下的,而是由內而外的。

我們以洋蔥架構為例:

可以看到,最關鍵的是中心的領域模型,它包括了所有的應用邏輯與規則。在這一層中不會直接引用技術實現,這樣就能夠確保在技術層面的改動不會影響到領域核心。

在領域層之外又包裹了領域服務層、應用服務層,而具體的技術實現則是被置于最外層的。

這種架構的好處就在于,它屏蔽掉了應用程序在UI層、DB層,以及各種中間件層的本質區別,所有的這些外部資源都被抽象成了對系統的輸入輸出,然后我們就能夠以一致的方式來處理不同的請求類型,并且,在與實際運行的設備和數據庫相隔離的情況下,也可以先行開發和測試。

在 DDD 的技術實現中,就用到了這種分層方式。

下圖是 Eric Evans 在其經典著作《領域驅動設計》中給出的一個典型的 DDD 系統所采用的分層架構:

在上圖中可以看到,整個架構劃分成了四個層,各層所表示的含義及其職責描述如下:

1、用戶接口層

這一層主要負責直接面向外部用戶或者系統,接收外部輸入,并返回結果。

用戶接口層是比較輕的一層,不含業務邏輯。可以做一些簡單的入參校驗,也可以記錄一下訪問日志,對異常進行統一的處理。同時,對返回值的封裝也應當在這層完成。

2、應用層

應用層,通常是用戶接口層的直接使用者。

但是在應用層中并不實現真正的業務規則,而是根據實際的 use case 來協調領域層提供的能力,也可以說,應用層主要做的是編排工作。

另外,應用層還負責了事務這個比較重要的功能。

3、領域層

領域層是整個業務的核心層。我們一般會使用充血模型來建模實際的領域對象。

同時,由于業務的核心價值在于其運作模式,而不是具體的技術手段或實現方式。因此,領域層的編碼原則上不允許依賴其他外部對象。

4、基礎設施層

基礎設施層,是在技術上具體的實現細節,它為上面各層提供通用的技術能力。

比如我們使用了哪種數據庫,數據是怎么存儲的,有沒有用到緩存、消息隊列等,都是在這一層要實現的。

對于這四個層次的劃分,大家通常都沒有太多的異議。但是在層與層之間的依賴關系上,后續又衍生出了很多的改良版本。比如在 IDDD 一書中,就給出了下圖所示的分層架構:

這里最大的不同,就是將領域層放到了整個架構的最下面,也即領域層之下就不再有任何的其他依賴。這么做是沒有問題的,但是最上面的基礎設施層看起來卻怪怪的。

在實際開發中,領域層的領域服務往往需要訪問持久化組件,以及基礎設施層中的其他組件,而對于持久化組件來說,不可避免地需要依賴領域層的實體對象。如此一來,領域層和基礎設施層,就產生了雙向依賴關系。

實際的解決方式,就是讓領域層和基礎設施層 都依賴一個統一的抽象,比如對于模型的持久化有 Repository 接口,對其他外部資源的訪問也可以通過接口的形式來解耦合。但是 Repository 接口跟其他接口 又有些不太一樣,Repository 因為需要參與到實體的整個生命周期中,所以在很多時候 Repository 都被看作是領域層中的一員。而對基礎設施層中其他組件的抽象,是不適合定義到領域層的。

?? DDD代碼模型

結合上面的描述,這個時候再來看代碼的組織形式,就比較清晰了。默認情況下,一個上下文對應了一個服務,我們這里以包含單個上下文的情況為例,給出如下的代碼目錄結構:

對上面的代碼結構做一個簡短的說明:

• Application,對應到架構里的應用層,其內可能包含一些 assembler 和 DTO,assembler 主要用于將領域對象轉換成返回需要的數據格式,這些數據格式以DTO的形式進行定義,這些DTO沒有任何的業務邏輯,就是單純的數據對象。

• domAIn,對應的是領域層,倉儲的接口也是放在這一層的。

• handler,對應的是架構里的用戶接口層,但其本質上還是屬于基礎設施層的一部分,這里單獨提出來也僅僅是為了凸顯它的重要性。在這一層,只可以直接訪問應用層。

• infra,對應的是基礎設施層,根據對不同資源的繼承需求,可以在 infra 下繼續分包。

• interfaces,是對基礎設施層中除持久化以外的中間件的抽象,也即我們在這里定義訪問中間件的接口,具體的實現還是放在基礎設施層。這里將接口單獨放到一個包中,為的是避免在領域層與應用層對基礎設施層的直接依賴,如此就通過依賴反轉解耦了具體的技術細節。

至此,我們就明確了代碼的分層組織結構,以及彼此之間的依賴關系。

我們在文章開頭提到的第二個問題是上下文的集成,在實際工作中,相信大家都會使用到微服務,這樣一來,如何集成就成為我們必須要考慮的問題。

02? 與其他上下文集成

上下文的集成無外乎兩種方式, 一種是通過RPC進行集成,另一種是通過領域事件進行集成。

通過領域事件集成,也就是領域事件的發送和消費,這個我們在前面的文章中已經做了比較詳細的介紹,這里不再贅述。

接下來主要說說通過 RPC 進行集成。

?? 開放主機與發布語言

我們先來看一個在 DDD 中,經常用來表示集成方式的示例圖:

其中,被集成方(A上下文,U 是 Upstream 的縮寫)采用了開放主機和發布語言的方式,而集成方(B上下文,D 是 Downstream 的縮寫)則使用了防腐層。幾個縮寫的含義如下:

• OHS(Open Host Service):開放主機服務,即定義一種協議,子系統可以通過該協議來訪問你的服務。

• PL(Published Language):發布語言,通常跟 OHS 一起使用,用于定義開放主機的協議。

• ACL(Anticorruption Layer):防腐層,一個上下文通過一些適配和轉換,來跟另一上下文交互。

我們平時大多數時候的開發工作,都是跟 Grpc/Kitex 等 RPC 框架打交道的,不同的框架在設計之初都會定義一份協議,只有符合協議要求的請求 才能被正確地識別和處理。比如 Grpc 使用 HTTP2 作為傳輸協議,而 Kitex 則主要使用自定義的 TTHeader 協議。

這些框架在使用上,一個共同特點就是需要通過 IDL(Interface description language) 來定義服務可以提供的能力。IDL 中可以定義多個接口,每個接口都有一個方法名,同時需要指定傳遞什么參數,返回什么數據。這樣的一份 IDL 就可以認為是我們為系統定義的發布語言。

還是以前面多次提到的商品服務為例,商品服務作為上下文集成中的被集成方,通過 thrift 定義了其可以提供的服務,比如下面是對 GetProductDetail 接口的定義:

所以,如果我們是一個服務的提供方,只要我們使用 Grpc/Kitex,那么就可以認為我們是使用 OSH 和 PL 方式來進行集成的。

?? 防腐層

防腐層一般用在下游上下文中,可以用來隔絕上游上下文中可能發生的變化。

在上面的例子中,商品服務提供了一個 GetProductDetail 接口,用以返回關于 Product 的全量信息。但是對于其他集成方來說,可能只是想拿到產品的很少一部分信息,比如在訂單服務中要展示訂單的詳情,而詳情只需要產品的圖片和名稱即可。

可以看到,作為服務的提供方,其具有追求普適性和靈活性的特點,而服務的調用方,在使用時卻想要能夠集中滿足特定需求的接口。

這種張力是導致在邊界上出現問題的主要原因,是無法避免的,但是卻是可以解決的,應對的方法就是使用防腐層。

從圖中可以看出,Subsystem A 和 Subsystem B 的調用關系并不是直接產生的,都要通過中間的一個ACL,ACL 除了負責執行具體的技術性調用,還將 A 和 B 的領域模型隔離開來,并承擔了彼此模型之間的翻譯轉換功能。

除此以外,還可以在 ACL 做緩存、兜底、開關等功能。

對于集成方來說,一般采用獨立接口的形式,接口的定義放在 interfaces 中,上面這個例子就可以這樣定義:

因為實現是跟具體的技術相關的,所以實現需要放到基礎設施層。整體的目錄層級如下:

具體的實現可以參考下面的代碼,簡單來說就是將通過 RPC 獲取到的上游模型,轉換為自己領域內的模型:

在傳統意義的防腐層實現中,會有一個適配器和一個對應的翻譯器,其中適配器的作用是適配對其他上下文的調用,而翻譯器就是將調用的結果轉換成本地上下文中的元素。

在這里,我們為了保持代碼的簡單,沒有特意聲明這樣兩個對象,rpc的方法在這里起到了適配器的作用,至于翻譯器,我們只是簡單的提出了一個方法,在方法名上做了特殊的前綴修飾。

最后,ProductRpcClient 會作為 ProductClient 的實現類,最終被注入到服務中。

03? CQRS 簡單實現

我們在看一些資料時,可能會看到有的地方叫CQS有的又叫CQRS。CQS 和 CQRS 都表示命令與查詢的分離,本質上沒有太大的區別。

CQS 是在《面向對象軟件架構》一書中提出來的概念,作者 Bertrand Meyer 認為,一個方法原則上不應該既修改數據又返回數據,所以就有了兩類方法:

1、查詢:返回數據,但不修改數據,不會產生副作用;

2、命令:修改數據,但不返回數據,存在副作用。

CQRS 是對 CQS 概念的升華,因為查詢端只返回數據,完全不修改數據,所以我們所有的查詢不需要走領域實體,甚至沒必要使用 ORM 框架,總之,我們可以通過各種手段來提升查詢的效率。

關于CQS與CQRS的更多信息,可以參考這篇文章,和這篇。

下圖是在各種技術文章中你會經??吹降囊粋€非常典型的 CQRS 架構示例:

圖中左側部分代表的是對 Command 的處理,右側是對 Query 的執行。很明顯的一個區別是,在 Query 中不再強制必須走領域模型,而是在應用層可以直接訪問基礎設施層。

在實際開發中,對 Query 的處理其實是比較靈活的,其目的無外乎是提高查詢的效率,另一方面也可以保證領域模型職責的單一。通常在查詢相對簡單的時候會復用領域模型,在稍微復雜時,會直接訪問底層的數據模型,如果查詢變得更加復雜,會將數據的存儲也獨立出來。

下面我們就依次說說這幾種情況要如何處理。

?? 復用領域模型

這種是最簡單的情況,對應的讀模型就是領域模型,要查詢的數據基本上都是模型里的屬性。

比如,我們有一個庫存的聚合根:

展示的數據如下:

這個時候,就可以通過 assembler 直接轉成對應的 view:

因為聚合根和倉儲是一一對應的,所以,在應用服務中直接通過 Repository 獲取領域模型即可:

?? 使用數據模型

在分頁查詢,或者是需要多個實體聚合查詢的場景,如果直接通過 Repository 獲取領域模型再組裝,可能會產生很多無關查詢,影響效率。

這個時候,可以根據要展示的數據直接使用數據模型,或者通過 sql 只獲取指定的某幾個字段。

比如,我們有 Product 和 Category 兩個聚合根,它們都包含了大量的屬性和業務邏輯,但是我們要展示的數據比較簡單:

這個時候就可以通過直接 sql 的形式來繞過領域模型:

?? 使用獨立的讀模型

這種情況下,一般對應的查詢場景都比較豐富,通常都會有一個獨立的查詢服務,各種數據在聚合處理之后統一放到查詢服務中。

如下所示,訂單在創建后,會使用 EventPublisher 來發布相應的事件:

在訂單查詢服務中,會對訂單創建這個事件進行監聽,當收到對應的消息時,會將訂單信息存儲到ES里。

如此一來,訂單數據就同時存在于 MySQL 以及 ES 中。而在查詢的時候會只通過 ES。

04? 結語

在這篇文章中,我們介紹了實踐領域驅動設計的時候應該如何組織代碼結構、如何進行上下文的集成,以及在復雜查詢場景中使用CQRS。這些內容我同樣是用腦圖的形式為你總結:

希望通過今天的講解,你能夠更游刃有余地應對開發中遇到的各種問題。但總地來說,DDD只是一種思想,所謂的分層架構也并不是事實上的標準,在實際應用時,還要結合自身的理解,可以適當地去創新或進行改進。

到目前為止,關于領域驅動設計的所有內容就都已經介紹完了。在下一篇文章中,我們會結合一個虛構的商城系統,帶你實戰領域驅動設計。

【技術專家】

于振

現于某大型互聯網公司,負責架構工作

曾就職于美團、快手等一線互聯網公司

分享到:
標簽:架構
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定