目錄
- DDD實踐:如何用DDD重構中臺業務模型?
- 領域建模:如何用事件風暴構建領域模型?
- 程式碼模型(上):如何使用DDD設計微服務程式碼模型?
- 程式碼模型(下):如何保證領域模型與程式碼模型的一致性?
- 邊界:微服務的各種邊界在架構演進中的作用?
- 檢視:如何實現服務和資料在微服務各層的協作?
- 從後端到前端:微服務後,前端如何設計?
- 知識點串講:基於DDD的微服務設計例項
- 基於DDD的微服務設計例項程式碼詳解
- 總結(一):微服務設計和拆分要堅持哪些原則?
- 總結(二):分散式架構關鍵設計10問
DDD實踐:如何用DDD重構中臺業務模型?
傳統企業應用分析
網際網路電商平臺和傳統核心應用,兩者面向的渠道和客戶不一樣,但銷售的產品卻很相似,它們之間的業務模型既有相同的地方,又有不同的地方。
- 核心能力的重複建設。由於銷售同質保險產品,二者在核心業務流程和功能上必然相似,因此在核心業務能力上存在功能重疊是不可避免的。
- 通用能力的重複建設。傳統核心應用的通用平臺大而全,通常會比較重。而網際網路電商平臺離不開這些通用能力的支撐,但為了保持敏捷性,一般會自己建設縮小版的通用功能,比如使用者、客戶等。
- 業務職能的分離建設。有一類業務功能,在網際網路電商平臺中建設了一部分,在傳統核心應用中也建設了一部分,二者功能不重疊而且還互補,組合在一起是一個完整的業務職能。
- 網際網路電商平臺和傳統核心功能前後完全獨立建設。
如何避免重複造輪子?
你需要站在企業高度,將重複的需要共享的通用能力、核心能力沉澱到中臺,將分離的業務能力重組為完整的業務板塊,構建可複用的中臺業務模型。前端個效能力歸前端,後端管理能力歸後臺。建立前、中、後臺邊界清晰,融合協作的企業級可複用的業務模型。
如何構建中臺業務模型?
1. 自頂向下的策略
這種策略是先做頂層設計,從最高領域逐級分解為中颱,分別建立領域模型,根據業務屬性分為通用中臺或核心中臺。領域建模過程主要基於業務現狀,暫時不考慮系統現狀。自頂向下的策略適用於全新的應用系統建設,或舊系統推倒重建的情況。
2. 自底向上的策略
這種策略是基於業務和系統現狀完成領域建模。首先分別完成系統所在業務域的領域建模;然後對齊業務域,找出具有同類或相似業務功能的領域模型,對比分析領域模型的差異,重組領域物件,重構領域模型。這個過程會沉澱公共和複用的業務能力,會將分散的業務模型整合。自底向上策略適用於遺留系統業務模型的演進式重構。
具體如何採用自底向上的策略來構建中臺業務模型,主要分為這樣三個步驟。
第一步:鎖定系統所在業務域,構建領域模型。
在這些領域模型的清單裡,我們可以看到二者之間有很多名稱相似的領域模型。深入分析後你會發現,這些名稱相似的領域模型存在業務能力重複,或者業務職能分散(比如移動支付和傳統支付)的問題。那在構建中臺業務模型時,你就需要重點關注它們,將這些不同領域模型中重複的業務能力沉澱到中臺業務模型中,將分散的領域模型整合到統一的中臺業務模型中,對外提供統一的共享的中臺服務。
第二步:對齊業務域,構建中臺業務模型。
首先我們可以將傳統核心的領域模型作為主領域模型,將網際網路電商領域模型作為輔助模型來構建中臺業務模型。然後再將網際網路電商中重複的能力沉澱到傳統核心的領域模型中,只保留自己的個效能力,比如訂單。中臺業務建模時,既要關注領域模型的完備性,也要關注不同渠道敏捷響應市場的要求。
客戶中臺業務模型的構建過程
網際網路電商客戶主要面向個人客戶,除了有個人客戶資訊管理功能外,基於營銷目的它還有客戶積分功能,因此它的領域模型有個人和積分兩個聚合。
而傳統核心客戶除了支援個人客戶外,還有單位和組織機構等團體客戶,它有個人和團體兩個領域模型。其中個人領域模型中除了個人客戶資訊管理功能外,還有個人客戶的評級、重複客戶的歸併和客戶的統一檢視等功能,因此它的領域模型有個人、檢視、評級和歸併四個聚合。
構建多業務域的中臺業務模型的過程,就是找出同一業務域內所有同類業務的領域模型,對比分析域內領域模型和聚合的差異和共同點,打破原有的模型,完成新的中臺業務模型重組或歸併的過程。
我們將網際網路電商和傳統核心的領域模型分解後,我們找到了五個與個人客戶領域相關的聚合,包括:個人、積分、評級、歸併和檢視。這五個聚合原來分別分散在網際網路電商和傳統核心的領域模型中,我們需要打破原有的領域模型,進行功能沉澱和聚合的重組,重新找出這些聚合的限界上下文,重構領域模型。
最終個人客戶的領域模型重構為:個人、歸併和檢視三個聚合重構為個人領域模型(客戶資訊管理),評級和積分兩個聚合重構為評級積分領域模型(面向個人客戶)。到這裡我們就完成了個人客戶領域模型的構建了。
總結成一句話就是:“分域建模型,找準基準域,劃定上下文,聚合重歸類。”
其它業務域重構後的中臺業務模型
第三步:中臺歸類,根據領域模型設計微服務。
完成中臺業務建模後,我們就有了下面這張圖。根據中臺下的領域模型就可以設計微服務了。
重構過程中的領域物件
傳統核心客戶領域模型重構之前,包含個人、團體和評級三個聚合,每個聚合內部都有自己的聚合根、實體、方法和領域服務等。
網際網路電商客戶領域模型重構前包含個人和積分兩個聚合,每個聚合包含了自己的領域物件、方法和領域服務等。
傳統核心和網際網路電商客戶領域模型重構成客戶中臺後,建立了個人、團體和評級積分三個領域模型。
部分領域物件可能會根據新的業務要求,從原來的聚合中分離,重組到其它聚合。新領域模型的領域物件,比如實體、領域服務等,在重組後可能還會根據新的業務場景和需求進行程式碼重構。
領域建模:如何用事件風暴構建領域模型?
事件風暴是一項團隊活動,領域專家與專案團隊通過頭腦風暴的形式,羅列出領域中所有的領域事件,整合之後形成最終的領域事件集合,然後對每一個事件,標註出導致該事件的命令,再為每一個事件標註出命令發起方的角色。命令可以是使用者發起,也可以是第三方系統呼叫或者定時器觸發等,最後對事件進行分類,整理出實體、聚合、聚合根以及限界上下文。而事件風暴正是 DDD 戰略設計中經常使用的一種方法,它可以快速分析和分解複雜的業務領域,完成領域建模。
事件風暴需要準備些什麼?
1. 事件風暴的參與者
事件風暴採用工作坊的方式,將專案團隊和領域專家聚集在一起,通過視覺化、高互動的方式一步一步將領域模型設計出來。
領域專家就是對業務或問題域有深刻見解的主題專家,他們非常瞭解業務和系統是怎麼做的,同時也深刻理解為什麼要這樣設計。
除了領域專家,事件風暴的其他參與者可以是 DDD 專家、架構師、產品經理、專案經理、開發人員和測試人員等專案團隊成員。
2. 事件風暴要準備的材料
事件風暴參與者會將自己的想法和意見寫在即時貼上,並將貼紙貼在牆上的合適位置,我們戲稱這個過程是“刷牆”。所以即時貼和水筆是必備材料,另外,你還可以準備一些膠帶或者磁扣,以便貼紙隨時能更換位置。
值得提醒一下的是,在這個過程中,我們要用不同顏色的貼紙區分領域行為。
3. 事件風暴的場地
你只需要一堵足夠長的牆和足夠大的空間就可以了。牆是用來貼紙的,大空間可以讓人四處走動,方便合作。撤掉會議桌和椅子的事件風暴,你會發現參與者們的效率更高。
4. 事件風暴分析的關注點
在領域建模的過程中,我們需要重點關注這類業務的語言和行為。比如某些業務動作或行為(事件)是否會觸發下一個業務動作,這個動作(事件)的輸入和輸出是什麼?是誰(實體)發出的什麼動作(命令),觸發了這個動作(事件)…我們可以從這些暗藏的詞彙中,分析出領域模型中的事件、命令和實體等領域物件。
如何用事件風暴構建領域模型?
領域建模的過程主要包括產品願景、業務場景分析、領域建模和微服務拆分與設計這幾個重要階段。
1. 產品願景
產品願景的主要目的是對產品頂層價值的設計,使產品目標使用者、核心價值、差異化競爭點等資訊達成一致,避免產品偏離方向。
2. 業務場景分析
場景分析是從使用者視角出發的,根據業務流程或使用者旅程,採用用例和場景分析,探索領域中的典型場景,找出領域事件、實體和命令等領域物件,支撐領域建模。事件風暴參與者要儘可能地遍歷所有業務細節,充分發表意見,不要遺漏業務要點。
使用者中臺有這樣三個典型的業務場景:
- 第一個是系統和崗位設定,設定系統中崗位的選單許可權;
- 第二個是使用者許可權配置,為使用者建立賬戶和密碼,設定使用者崗位;
- 第三個是使用者登入系統和許可權校驗,生成使用者登入和操作日誌。
我們可以按照業務流程,一步一步搜尋使用者業務流程中的關鍵領域事件,比如崗位已建立,使用者已建立等事件。再找出什麼行為會引起這些領域事件,這些行為可能是一個或若干個命令組合在一起產生的,比如建立使用者時,第一個命令是從公司 HR 系統中獲取使用者資訊,第二個命令是根據 HR 的員工資訊在使用者中臺建立使用者,建立完使用者後就會產生使用者已建立的領域事件。當然這個領域事件可能會觸發下一步的操作,比如釋出到郵件系統通知使用者已建立,但也可能到此就結束了,你需要根據具體情況來分析是否還有下一步的操作。
3. 領域建模
領域建模時,我們會根據場景分析過程中產生的領域物件,比如命令、事件等之間關係,找出產生命令的實體,分析實體之間的依賴關係組成聚合,為聚合劃定限界上下文,建立領域模型以及模型之間的依賴。領域模型利用限界上下文向上可以指導微服務設計,通過聚合向下可以指導聚合根、實體和值物件的設計。
具體可以分為這樣三步。
- 第一步:從命令和事件中提取產生這些行為的實體
- 第二步:根據聚合根的管理性質從七個實體中找出聚合根
- 第三步:劃定限界上下文,根據上下文語義將聚合歸類
到這裡我們就完成了使用者中臺領域模型的構建了。那由於領域建模的過程中產生的領域物件實在太多了,我們可以藉助表格來記錄。
4. 微服務拆分與設計
原則上一個領域模型就可以設計為一個微服務,但由於領域建模時只考慮了業務因素,沒有考慮微服務落地時的技術、團隊以及執行環境等非業務因素,因此在微服務拆分與設計時,我們不能簡單地將領域模型作為拆分微服務的唯一標準,它只能作為微服務拆分的一個重要依據。
使用者中臺微服務設計如果不考慮非業務因素,我們完全可以按照領域模型與微服務一對一的關係來設計,將使用者中臺設計為:使用者、認證和許可權三個微服務。但如果使用者日誌資料量巨大,大到需要採用大資料技術來實現,這時使用者資訊聚合與使用者日誌聚合就會有技術異構。雖然在領域建模時,我們將他們放在一個了領域模型內,但如果考慮技術異構,這兩個聚合就不適合放到同一個微服務裡了。我們可以以聚合作為拆分單位,將使用者基本資訊管理和使用者日誌管理拆分為兩個技術異構的微服務,分別用不同的技術來實現它們。
程式碼模型(上):如何使用DDD設計微服務程式碼模型?
DDD 分層架構與微服務程式碼模型
業務邏輯從領域層、應用層到使用者介面層逐層封裝和協作,對外提供靈活的服務,既實現了各層的分工,又實現了各層的協作。因此,毋庸置疑,DDD 分層架構模型就是設計微服務程式碼模型的最佳依據。
微服務程式碼模型
微服務一級目錄結構
各層目錄結構
1. 使用者介面層
- Assembler:實現 DTO 與領域物件之間的相互轉換和資料交換。一般來說 Assembler 與 DTO 總是一同出現。
- Dto:它是資料傳輸的載體,內部不存在任何業務邏輯,我們可以通過 DTO 把內部的領域物件與外界隔離。
- Facade:提供較粗粒度的呼叫介面,將使用者請求委派給一個或多個應用服務進行處理。
2. 應用層
雖然應用層和領域層都可以進行事件的釋出和處理,但為了實現事件的統一管理,我建議你將微服務內所有事件的釋出和訂閱的處理都統一放到應用層,事件相關的核心業務邏輯實現放在領域層。
3. 領域層
按照 DDD 分層架構,倉儲實現本應該屬於基礎層程式碼,但為了在微服務架構演進時,保證程式碼拆分和重組的便利性,我是把聚合倉儲實現的程式碼放到了聚合包內。這樣,如果需求或者設計發生變化導致聚合需要拆分或重組時,我們就可以將包括核心業務邏輯和倉儲程式碼的聚合包整體遷移,輕鬆實現微服務架構演進。
4. 基礎層
Util:主要存放平臺、開發框架、訊息、資料庫、快取、檔案、匯流排、閘道器、第三方類庫、通用演算法等基礎程式碼,你可以為不同的資源類別建立不同的子目錄。
程式碼模型總目錄結構
總結
那關於程式碼模型我還需要強調兩點內容。
第一點:聚合之間的程式碼邊界一定要清晰。聚合之間的服務呼叫和資料關聯應該是儘可能的鬆耦合和低關聯,聚合之間的服務呼叫應該通過上層的應用層組合實現呼叫,原則上不允許聚合之間直接呼叫領域服務。這種鬆耦合的程式碼關聯,在以後業務發展和需求變更時,可以很方便地實現業務功能和聚合程式碼的重組,在微服務架構演進中將會起到非常重要的作用。
第二點:你一定要有程式碼分層的概念。寫程式碼時一定要搞清楚程式碼的職責,將它放在職責對應的程式碼目錄內。應用層程式碼主要完成服務組合和編排,以及聚合之間的協作,它是很薄的一層,不應該有核心領域邏輯程式碼。領域層是業務的核心,領域模型的核心邏輯程式碼一定要在領域層實現。如果將核心領域邏輯程式碼放到應用層,你的基於 DDD 分層架構模型的微服務慢慢就會演變成傳統的三層架構模型了。
程式碼模型(下):如何保證領域模型與程式碼模型的一致性?
DDD 強調先構建領域模型然後設計微服務,以保證領域模型和微服務的一體性,因此我們不能脫離領域模型來談微服務的設計和落地。但在構建領域模型時,我們往往是站在業務視角的,並且有些領域物件還帶著業務語言。我們還需要將領域模型作為微服務設計的輸入,對領域物件進行設計和轉換,讓領域物件與程式碼物件建立對映關係。
領域物件的整理
我們第一個重要的工作就是,整理事件風暴過程中產生的各個領域物件,比如:聚合、實體、命令和領域事件等內容,將這些領域物件和業務行為記錄到下面的表格中。
從領域模型到微服務的設計
從領域模型到微服務落地,我們還需要做進一步的設計和分析。事件風暴中提取的領域物件,還需要經過使用者故事或領域故事分析,以及微服務設計,才能用於微服務系統開發。
領域層的領域物件
下面我們就來看一下這些領域物件是怎麼得來的?
1. 設計實體
大多數情況下,領域模型的業務實體與微服務的資料庫實體是一一對應的。但某些領域模型的實體在微服務設計時,可能會被設計為多個資料實體,或者實體的某些屬性被設計為值物件。
在分層架構裡,實體採用充血模型,在實體類內實現實體的全部業務邏輯。這些不同的實體都有自己的方法和業務行為,比如地址實體有新增和修改地址的方法,銀行賬號實體有新增和修改銀行賬號的方法。
2. 找出聚合根
聚合根來源於領域模型,在個人客戶聚合裡,個人客戶這個實體是聚合根,它負責管理地址、電話以及銀行賬號的生命週期。個人客戶聚合根通過工廠和倉儲模式,實現聚合內地址、銀行賬號等實體和值物件資料的初始化和持久化。
3. 設計值物件
根據需要將某些實體的某些屬性或屬性集設計為值物件。值物件類放在程式碼模型的 Entity 目錄結構下。在個人客戶聚合中,客戶擁有客戶證件型別,它是以列舉值的形式存在,所以將它設計為值物件。
4. 設計領域事件
如果領域模型中領域事件會觸發下一步的業務操作,我們就需要設計領域事件。首先確定領域事件發生在微服務內還是微服務之間。然後設計事件實體物件,事件的釋出和訂閱機制,以及事件的處理機制。判斷是否需要引入事件匯流排或訊息中介軟體。
5. 設計領域服務
如果一個業務動作或行為跨多個實體,我們就需要設計領域服務。領域服務通過對多個實體和實體方法進行組合,完成核心業務邏輯。你可以認為領域服務是位於實體方法之上和應用服務之下的一層業務邏輯。
按照嚴格分層架構層的依賴關係,如果實體的方法需要暴露給應用層,它需要封裝成領域服務後才可以被應用服務呼叫。所以如果有的實體方法需要被前端應用呼叫,我們會將它封裝成領域服務,然後再封裝為應用服務。
6. 設計倉儲
每一個聚合都有一個倉儲,倉儲主要用來完成資料查詢和持久化操作。倉儲包括倉儲的介面和倉儲實現,通過依賴倒置實現應用業務邏輯與資料庫資源邏輯的解耦。
應用層的領域物件
應用層的主要領域物件是應用服務和事件的釋出以及訂閱。
在嚴格分層架構模式下,不允許服務的跨層呼叫,每個服務只能呼叫它的下一層服務。服務從下到上依次為:實體方法、領域服務和應用服務。
1. 實體方法的封裝
實體方法是最底層的原子業務邏輯。如果單一實體的方法需要被跨層呼叫,你可以將它封裝成領域服務,這樣封裝的領域服務就可以被應用服務呼叫和編排了。如果它還需要被使用者介面層呼叫,你還需要將這個領域服務封裝成應用服務。經過逐層服務封裝,實體方法就可以暴露給上面不同的層,實現跨層呼叫。
2. 領域服務的組合和封裝
領域服務會對多個實體和實體方法進行組合和編排,供應用服務呼叫。如果它需要暴露給使用者介面層,領域服務就需要封裝成應用服務。
3. 應用服務的組合和編排
應用服務會對多個領域服務進行組合和編排,暴露給使用者介面層,供前端應用呼叫。
領域物件與微服務程式碼物件的對映
在完成上面的分析和設計後,我們就可以建立像下圖一樣的,領域物件與微服務程式碼物件的對映關係了。
典型的領域模型
我們看一下下面這個圖,我們對個人客戶聚合做了進一步的分析。提取了個人客戶表單這個聚合根,形成了客戶型別值物件,以及電話、地址、銀行賬號等實體,為實體方法和服務做了封裝和分層,建立了領域物件的關聯和依賴關係,還有倉儲等設計。關鍵是這個過程,我們建立了領域物件與微服務程式碼物件的對映關係。
在建立這種對映關係後,我們就可以得到如下圖的微服務程式碼結構了。
非典型領域模型
那對於這類非典型模型,我們怎麼辦?
我們還是可以借鑑聚合的思想,仍然用聚合來定義這部分功能,並採用與典型領域模型同樣的分析方法,建立實體的屬性和方法,對方法和服務進行封裝和分層設計,設計倉儲,建立領域物件之間的依賴關係。唯一可惜的就是我們依然找不到聚合根,不過也沒關係,除了聚合根管理功能外,我們還可以用 DDD 的其它設計方法。
邊界:微服務的各種邊界在架構演進中的作用?
微服務的設計要涉及到邏輯邊界、物理邊界和程式碼邊界等等。
演進式架構
演進式架構就是以支援增量的、非破壞的變更作為第一原則,同時支援在應用程式結構層面的多維度變化。
隨著業務的發展或需求的變更,在不斷重新拆分或者組合成新的微服務的過程中,不會大幅增加軟體開發和維護的成本,並且這個架構演進的過程是非常輕鬆、簡單的。
這也是微服務設計的重點,就是看微服務設計是否能夠支援架構長期、輕鬆的演進。
微服務還是小單體?
這種單體式微服務只定義了一個維度的邊界,也就是微服務之間的物理邊界,本質上還是單體架構模式。微服務設計時要考慮的不僅僅只有這一個邊界,別忘了還要定義好微服務內的邏輯邊界和程式碼邊界,這樣才能得到你想要的結果。
微服務邊界的作用
在事件風暴中,我們會梳理出業務過程中的使用者操作、事件以及外部依賴關係等,根據這些要素梳理出實體等領域物件。根據實體物件之間的業務關聯性,將業務緊密相關的多個實體進行組合形成聚合,聚合之間是第一層邊界。根據業務及語義邊界等因素將一個或者多個聚合劃定在一個限界上下文內,形成領域模型,限界上下文之間的邊界是第二層邊界。
邏輯邊界主要定義同一業務領域或應用內緊密關聯的物件所組成的不同聚類的組合之間的邊界。事件風暴對不同實體物件進行關聯和聚類分析後,會產生多個聚合和限界上下文,它們一起組成這個領域的領域模型。微服務內聚合之間的邊界就是邏輯邊界。一般來說微服務會有一個以上的聚合,在開發過程中不同聚合的程式碼隔離在不同的聚合程式碼目錄中。
隨著業務的快速發展,如果某一個微服務遇到了高效能挑戰,需要將部分業務能力獨立出去,我們就可以以聚合為單位,將聚合程式碼拆分獨立為一個新的微服務,這樣就可以很容易地實現微服務的拆分。
另外,我們也可以對多個微服務內有相似功能的聚合進行功能和程式碼重組,組合為新的聚合和微服務,獨立為通用微服務。
物理邊界主要從部署和執行的視角來定義微服務之間的邊界。不同微服務部署位置和執行環境是相互物理隔離的,分別執行在不同的程式中。這種邊界就是微服務之間的物理邊界。
程式碼邊界主要用於微服務內的不同職能程式碼之間的隔離。微服務開發過程中會根據程式碼模型建立相應的程式碼目錄,實現不同功能程式碼的隔離。由於領域模型與程式碼模型的對映關係,程式碼邊界直接體現出業務邊界。程式碼邊界可以控制程式碼重組的影響範圍,避免業務和服務之間的相互影響。微服務如果需要進行功能重組,只需要以聚合程式碼為單位進行重組就可以了。
正確理解微服務的邊界
微服務的拆分可以參考領域模型,也可以參考聚合,因為聚合是可以拆分為微服務的最小單位的。但實施過程是否一定要做到邏輯邊界與物理邊界一致性呢?也就是說聚合是否也一定要設計成微服務呢?答案是不一定的,這裡就涉及到微服務過度拆分的問題了。
微服務的過度拆分會使軟體維護成本上升,比如:整合成本、釋出成本、運維成本以及監控和定位問題的成本等。在專案建設初期,如果你不具備較強的微服務管理能力,那就不宜將微服務拆分過細。當我們具備一定的能力以後,且微服務內部的邏輯和程式碼邊界也很清晰,你就可以隨時根據需要,拆分出新的微服務,實現微服務的架構演進了。
檢視:如何實現服務和資料在微服務各層的協作?
服務的協作
1. 服務的型別
分層架構中的服務。按照分層架構設計出來的微服務,其內部有 Facade 服務、應用服務、領域服務和基礎服務。
- Facade 服務:位於使用者介面層,包括介面和實現兩部分。用於處理使用者傳送的 Restful 請求和解析使用者輸入的配置檔案等,並將資料傳遞給應用層。或者在獲取到應用層資料後,將 DO 組裝成 DTO,將資料傳輸到前端應用。
- 應用服務:位於應用層。用來表述應用和使用者行為,負責服務的組合、編排和轉發,負責處理業務用例的執行順序以及結果拼裝,對外提供粗粒度的服務。
- 領域服務:位於領域層。領域服務封裝核心的業務邏輯,實現需要多個實體協作的核心領域邏輯。它對多個實體或方法的業務邏輯進行組合或編排,或者在嚴格分層架構中對實體方法進行封裝,以領域服務的方式供應用層呼叫。
- 基礎服務:位於基礎層。提供基礎資源服務(比如資料庫、快取等),實現各層的解耦,降低外部資源變化對業務應用邏輯的影響。基礎服務主要為倉儲服務,通過依賴倒置提供基礎資源服務。領域服務和應用服務都可以呼叫倉儲服務介面,通過倉儲服務實現資料持久化。2. 服務的呼叫
2. 服務的呼叫
微服務的服務呼叫包括三類主要場景:微服務內跨層服務呼叫,微服務之間服務呼叫和領域事件驅動。
微服務內跨層服務呼叫
微服務架構下往往採用前後端分離的設計模式,前端應用獨立部署。前端應用呼叫釋出在 API 閘道器上的 Facade 服務,Facade 定向到應用服務。應用服務作為服務組織和編排者,它的服務呼叫有這樣兩種路徑:
- 第一種是應用服務呼叫並組裝領域服務。此時領域服務會組裝實體和實體方法,實現核心領域邏輯。領域服務通過倉儲服務獲取持久化資料物件,完成實體資料初始化。
- 第二種是應用服務直接呼叫倉儲服務。這種方式主要針對像快取、檔案等型別的基礎層資料訪問。這類資料主要是查詢操作,沒有太多的領域邏輯,不經過領域層,不涉及資料庫持久化物件。
微服務之間的服務呼叫
微服務之間的應用服務可以直接訪問,也可以通過 API 閘道器訪問。由於跨微服務操作,在進行資料新增和修改操作時,你需關注分散式事務,保證資料的一致性。
領域事件驅動
領域事件驅動包括微服務內和微服務之間的事件。微服務內通過事件匯流排(EventBus)完成聚合之間的非同步處理。微服務之間通過訊息中介軟體完成。非同步化的領域事件驅動機制是一種間接的服務訪問方式。
當應用服務業務邏輯處理完成後,如果發生領域事件,可呼叫事件釋出服務,完成事件釋出。當接收到訂閱的主題資料時,事件訂閱服務會呼叫事件處理領域服務,完成進一步的業務操作。
3. 服務的封裝與組合
微服務的服務是從領域層逐級向上封裝、組合和暴露的。
領域層
領域層實現核心業務邏輯,負責表達領域模型業務概念、業務狀態和業務規則。主要的服務形態有實體方法和領域服務。
DDD 提倡富領域模型,儘量將業務邏輯歸屬到實體物件上,實在無法歸屬的部分則設計成領域服務。領域服務會對多個實體或實體方法進行組裝和編排,實現跨多個實體的複雜核心業務邏輯。
應用層
應用層的主要服務形態有:應用服務、事件釋出和訂閱服務。
為了實現微服務內聚合之間的解耦,聚合之間的服務呼叫和資料互動應通過應用服務來完成。原則上我們應該禁止聚合之間的領域服務直接呼叫和聚合之間的資料表關聯。
4. 兩種分層架構的服務依賴關係
鬆散分層架構的服務依賴
在鬆散分層架構中,領域層的實體方法和領域服務可以直接暴露給應用層和使用者介面層。鬆散分層架構的服務依賴關係,無需逐級封裝,可以快速暴露給上層。
嚴格分層架構的服務依賴
在嚴格分層架構中,每一層服務只能向緊鄰的上一層提供服務。雖然實體、實體方法和領域服務都在領域層,但實體和實體方法只能暴露給領域服務,領域服務只能暴露給應用服務。
資料物件檢視
我們先來看一下微服務內有哪些型別的資料物件?它們是如何協作和轉換的?
- 資料持久化物件 PO(Persistent Object),與資料庫結構一一對映,是資料持久化過程中的資料載體。
- 領域物件 DO(Domain Object),微服務執行時的實體,是核心業務的載體。
- 資料傳輸物件 DTO(Data Transfer Object),用於前端與應用層或者微服務之間的資料組裝和傳輸,是應用之間資料傳輸的載體。
- 檢視物件 VO(View Object),用於封裝展示層指定頁面或元件的資料。
從後端到前端:微服務後,前端如何設計?
微服務架構通常採用前後端分離的設計方式。作為企業級的中臺,在完成單體應用拆分和微服務建設後,前端專案團隊會同時面對多箇中臺微服務專案團隊,這時候的前端人員就猶如維修電工一樣了。
要從一定程度上解決上述問題,我們是不是可以考慮先有效降低前端整合的複雜度呢?先做到前端聚合,後端解耦
從單體前端到微前端
在前端設計時我們需要遵循單一職責和複用原則,按照領域模型和微服務邊界,將前端頁面進行拆分。同時構建多個可以獨立部署、完全自治、鬆耦合的頁面組合,其中每個組合只負責特定業務單元的 UI 元素和功能,這些頁面組合就是微前端。
微前端與微服務一樣,都是希望將單體應用,按照規則拆分,並重組為多個可以獨立開發、獨立測試、獨立部署和獨立運維,鬆耦合的微前端或者微服務。以適應業務快速變化及分散式多團隊並行開發的要求。
業務單元的組合形態
我們可以參照領域模型和微服務邊界,建立與微服務對應的前端操作介面,將它與微服務組成業務單元,以業務元件的方式對外提供服務。業務單元包括微前端和微服務,可以獨立開發、測試、部署和運維,可以自包含地完成領域模型中部分或全部的業務功能。
我們看一下下面這個圖。一個虛框就是一個業務單元,微前端和微服務獨立部署,業務單元內的微前端和微服務已完成前後端整合。你可以將這個業務單元理解為一個特定業務領域的元件。業務單元可以有多種組合方式,以實現不同的業務目標。
記住一點:微前端不宜與過多的微服務組合,否則容易變成單體前端。
微前端的整合方式
我們看一下下面這個圖,微前端位於前端主頁面和微服務之間,它需要與兩者完成整合。
1. 微前端與前端主頁面的整合
前端主頁面是企業級的前端頁面,微前端是業務單元的前端頁面。微前端通過主頁面的微前端載入器,利用頁面路由和動態載入等技術,將特定業務單元的微前端頁面動態載入到前端主頁面,實現前端主頁面與微前端頁面的“拼圖式”整合。
微前端完成開發、整合和部署後,在前端主頁面完成微前端註冊以及頁面路由配置,即可實現動態載入微前端頁面。
2. 微前端與微服務的整合
微前端與微服務獨立開發,獨立部署。在微前端註冊到前端主頁面前,微前端需要與微服務完成整合。它的整合方式與傳統前後端分離的整合方式沒有差異。微服務將服務釋出到 API 閘道器,微前端呼叫釋出在 API 閘道器中的服務,即完成業務單元內的前後端整合。
團隊職責邊界
前端專案團隊專注於前端整合主頁面與微前端的整合,完成前端主頁面的企業級主流程的頁面和流程編排以及微前端頁面的動態載入,確保主流程業務邏輯和流程正確。前端專案除了要負責企業內頁面風格的整體風格設計、業務流程的流轉和控制外,還需要負責微前端頁面動態載入、微前端註冊、頁面路由和頁面資料共享等前端技術的實現。
中臺專案團隊完成業務單元元件的開發、測試和整合,確保業務單元內的業務邏輯、頁面和流程正確,向外提供包含頁面邏輯和業務邏輯的業務單元元件。
這樣,前端專案團隊只需要完成企業級前端主頁面與業務單元的融合,前端只關注前端主頁面與微前端頁面之間的整合。
中臺專案團隊關注業務單元功能的完整性和自包含能力,完成業務單元內微服務和微前端開發、整合和部署,提供業務單元元件。
一個有關保險微前端設計的案例
保險集團為了統一運營,會實現壽險、財險等集團化的全險種銷售。這樣前端專案團隊就需要用一個前端應用,整合非常多的不同產品的核心中臺微服務,前端應用與中臺微服務之間的整合將會更復雜。
如果仍然採用傳統的單體前端模式,將會面臨比較大的困難。
- 第一是前端頁面開發和設計的複雜性。
- 第二是前端與微服務整合的複雜性。
- 第三是前後端軟體版本的協同釋出。
那如何用一個前端應用實現全險種產品銷售呢?怎樣設計才能降低整合的複雜度,實現前端介面融合,後端中臺解耦呢?
我們看一下下面這個圖。我們借鑑了電商的訂單模式實現保險產品的全險種訂單化銷售,在一個前端主頁面可以將所有業務流程和業務操作無縫串聯起來。雖然後端有很多業務單元(包含微服務和微前端),但使用者始終感覺是在一個前端應用中操作。
微前端
每個微服務都有自己的微前端頁面,實現領域模型的微服務前端頁面操作。
業務單元
微服務與微前端組合為一個業務單元。由一箇中臺團隊完成業務單元的開發、整合、測試和部署,確保業務單元內頁面操作和業務邏輯正確。
前端主頁面
前端主頁面類似門戶,包括頁面導航以及部分通用的常駐主頁面的共享頁面,比如購物車。前端主頁面和所有微前端應統一介面風格,符合統一的前端整合規範。按照正確的業務邏輯和規則,動態載入不同業務單元的微前端頁面。前端主頁面作為一個整體,協調核心和通用業務單元的微前端頁面,完成業務操作和業務流程,提供全險種銷售接觸介面,包括商品目錄、錄單、購物車、訂單、支付等操作。
雖然後端有很多業務單元在支援,但使用者所有的頁面操作和流轉是在一個前端主頁面完成的。在進行全險種的訂單化銷售時,使用者始終感覺是在操作一個系統。這種設計方式很好地體現了前端的融合和中臺的解耦。
微前端和業務單元化的設計模式可以減輕企業級中臺,前後端應用開發和整合的複雜度,真正實現前端融合和中臺解耦。它的主要價值和意義如下:
- 前端整合簡單
- 專案職責專一
- 隔離和依賴性
- 降低溝通和測試成本
- 更敏捷地釋出
- 降低技術敏感性
- 高度複用性
知識點串講:基於DDD的微服務設計例項
為了更好地理解 DDD 的設計流程,今天我會用一個專案來帶你瞭解 DDD 的戰略設計和戰術設計,走一遍從領域建模到微服務設計的全過程,一起掌握 DDD 的主要設計流程和關鍵點。
專案基本資訊
專案的目標是實現線上請假和考勤管理。功能描述如下:
1、請假人填寫請假單提交審批,根據請假人身份、請假型別和請假天數進行校驗,根據審批規則逐級遞交上級審批,逐級核批通過則完成審批,否則審批不通過退回申請人。
2、根據考勤規則,核銷請假資料後,對考勤資料進行校驗,輸出考勤統計。
戰略設計
戰略設計是根據使用者旅程分析,找出領域物件和聚合根,對實體和值物件進行聚類組成聚合,劃分限界上下文,建立領域模型的過程。
1. 產品願景
產品願景是對產品頂層價值設計,對產品目標使用者、核心價值、差異化競爭點等資訊達成一致,避免產品偏離方向。
事件風暴時,所有參與者針對每一個要點,在貼紙上寫出自己的意見,貼到白板上。事件風暴主持者會對每個貼紙,討論並對發散的意見進行收斂和統一,形成下面的產品願景圖。
我們把這個產品願景圖整理成一段文字就是:為了滿足內外部人員,他們的線上請假、自動考勤統計和外部人員管理的需求,我們建設這個線上請假考勤系統,它是一個線上請假平臺,可以自動考勤統計。它可以同時支援內外網請假,同時管理內外部人員請假和定期考勤分析,而不像 HR 系統,只管理內部人員,且只能內網使用。我們的產品內外網皆可使用,可實現內外部人員無差異管理。
通過產品願景分析,專案團隊統一了系統名稱——線上請假考勤系統,明確了專案目標和關鍵功能,與競品(HR)的關鍵差異以及自己的優勢和核心競爭力等。
2. 場景分析
場景分析是從使用者視角出發,探索業務領域中的典型場景,產出領域中需要支撐的場景分類、用例操作以及不同子域之間的依賴關係,用以支撐領域建模。
下面我就以請假和人員兩個場景作為示例。
第一個場景:請假
使用者:請假人
- 請假人登入系統:從許可權微服務獲取請假人資訊和許可權資料,完成登入認證。
- 建立請假單:開啟請假頁面,選擇請假型別和起始時間,錄入請假資訊。儲存並建立請假單,提交請假審批。
- 修改請假單:查詢請假單,開啟請假頁面,修改請假單,提交請假審批。
- 提交審批:獲取審批規則,根據審批規則,從人員組織關係中獲取審批人,給請假單分配審批人。
第二個場景:審批
使用者:審批人
- 審批人登入系統:從許可權微服務獲取審批人資訊和許可權資料,完成登入認證。
- 獲取請假單:獲取審批人名下請假單,選擇請假單。
- 審批:填寫審批意見。逐級審批:如果還需要上級審批,根據審批規則,從人員組織關係中獲取審批人,給請假單分配審批人。
- 重複以上 4 步。最後審批人完成審批。
完成審批後,產生請假審批已通過領域事件。後續有兩個進一步的業務操作:傳送請假審批已通過的通知,通知郵件系統告知請假人;將請假資料傳送到考勤以便核銷。
下面這個圖是人員組織關係場景分析結果圖
3. 領域建模
領域建模是通過對業務和問題域進行分析,建立領域模型。向上通過限界上下文指導微服務邊界設計,向下通過聚合指導實體物件設計。
領域建模是一個收斂的過程,分三步:
- 第一步找出領域實體和值物件等領域物件;
- 第二步找出聚合根,根據實體、值物件與聚合根的依賴關係,建立聚合;
- 第三步根據業務及語義邊界等因素,定義限界上下文。
下面我們就逐步詳細講解一下。
第一步:找出實體和值物件等領域物件
根據場景分析,分析並找出發起或產生這些命令或領域事件的實體和值物件,將與實體或值物件有關的命令和事件聚集到實體。
第二步:定義聚合
定義聚合前,先找出聚合根。從上面的實體中,我們可以找出“請假單”和“人員”兩個聚合根。然後找出與聚合根緊密依賴的實體和值物件。我們發現審批意見、審批規則和請假單緊密關聯,組織關係和人員緊密關聯。
找出這些實體的關係後,我們發現還有刷卡明細、考勤明細和考勤統計,這幾個實體沒有聚合根。這種情形在領域建模時你會經常遇到,對於這類場景我們需要分情況特殊處理。
刷卡明細、考勤明細和考勤統計這幾個實體,它們之間相互獨立,找不出聚合根,不是富領域模型,但它們一起完成考勤業務邏輯,具有很高的業務內聚性。我們將這幾個業務關聯緊密的實體,放在一個考勤聚合內。在微服務設計時,我們依然採用 DDD 的設計和分析方法。由於沒有聚合根來管理聚合內的實體,我們可以用傳統的方法來管理實體。
經過分析,我們建立了請假、人員組織關係和考勤三個聚合。其中請假聚合有請假單、審批意見實體和審批規則等值物件。人員組織關係聚合有人員和組織關係等實體。考勤聚合有刷卡明細、考勤明細和考勤統計等實體。
第三步:定義限界上下文
由於人員組織關係聚合與請假聚合,共同完成請假的業務功能,兩者在請假的限界上下文內。考勤聚合則單獨構成考勤統計限界上下文。因此我們為業務劃分請假和考勤統計兩個限界上下文,建立請假和考勤兩個領域模型。
4. 微服務的拆分
理論上一個限界上下文就可以設計為一個微服務,但還需要綜合考慮多種外部因素,比如:職責單一性、敏態與穩態業務分離、非功能性需求(如彈性伸縮、版本釋出頻率和安全等要求)、軟體包大小、團隊溝通效率和技術異構等非業務要素。
在這個專案,我們劃分微服務主要考慮職責單一性原則。因此根據限界上下文就可以拆分為請假和考勤兩個微服務。其中請假微服務包含人員組織關係和請假兩個聚合,考勤微服務包含考勤聚合。
到這裡,戰略設計就結束了。通過戰略設計,我們建立了領域模型,劃分了微服務邊界。下一步就是戰術設計了,也就是微服務設計。下面我們以請假微服務為例,講解其設計過程。
戰術設計
戰術設計是根據領域模型進行微服務設計的過程。這個階段主要梳理微服務內的領域物件,梳理領域物件之間的關係,確定它們在程式碼模型和分層架構中的位置,建立領域模型與微服務模型的對映關係,以及服務之間的依賴關係。
戰術設計包括以下兩個階段:分析微服務領域物件和設計微服務程式碼結構。
1. 分析微服務領域物件
服務的識別和設計
事件風暴的命令是外部的一些操作和業務行為,也是微服務對外提供的能力。它往往與微服務的應用服務或者領域服務對應。我們可以將命令作為服務識別和設計的起點。具體步驟如下:
- 根據命令設計應用服務,確定應用服務的功能,服務集合,組合和編排方式。服務集合中的服務包括領域服務或其它微服務的應用服務。
- 根據應用服務功能要求設計領域服務,定義領域服務。這裡需要注意:應用服務可能是由多個聚合的領域服務組合而成的。
- 根據領域服務的功能,確定領域服務內的實體以及功能。
- 設計實體基本屬性和方法。
另外,我們還要考慮領域事件的非同步化處理。
我以提交審批這個動作為例,來說明服務的識別和設計。提交審批的大體流程是:
- 根據請假型別和時長,查詢請假審批規則,獲取下一步審批人的角色。
- 根據審批角色從人員組織關係中查詢下一審批人。
- 為請假單分配審批人,並將審批規則儲存至請假單。
- 通過分析,我們需要在應用層和領域層設計以下服務和方法。
應用層:提交審批應用服務。
領域層:領域服務有查詢審批規則、修改請假流程資訊服務以及根據審批規則查詢審批人服務,分別位於請假和人員組織關係聚合。請假單實體有修改請假流程資訊方法,審批規則值物件有查詢審批規則方法。人員實體有根據審批規則查詢審批人方法。下圖是我們分析出來的服務以及它們之間的依賴關係。
服務的識別和設計過程就是這樣了,我們再來設計一下聚合內的物件。
聚合中的物件
在請假單聚合中,聚合根是請假單。
請假單經多級稽核後,會產生多條審批意見,為了方便查詢,我們可以將審批意見設計為實體。請假審批通過後,會產生請假審批通過的領域事件,因此還會有請假事件實體。請假聚合有以下實體:審批意見(記錄審批人、審批狀態和審批意見)和請假事件實體。
我們再來分析一下請假單聚合的值物件。請假人和下一審批人資料來源於人員組織關係聚合中的人員實體,可設計為值物件。人員型別、請假型別和審批狀態是列舉值型別,可設計為值物件。確定請假審批規則後,審批規則也可作為請假單的值物件。請假單聚合將包含以下值物件:請假人、人員型別、請假型別、下一審批人、審批狀態和審批規則。
綜上,我們就可以畫出請假聚合物件關係圖了。
在人員組織關係聚合中,我們可以建立人員之間的組織關係,通過組織關係型別找到上級審批領導。它的聚合根是人員,實體有組織關係(包括組織關係型別和上級審批領導),其中組織關係型別(如專案經理、處長、總經理等)是值物件。上級審批領導來源於人員聚合根,可設計為值物件。人員組織關係聚合將包含以下值物件:組織關係型別、上級審批領導。
綜上,我們又可以畫出人員組織關係聚合物件關係圖了。
微服務內的物件清單
在確定各領域物件的屬性後,我們就可以設計各領域物件在程式碼模型中的程式碼物件(包括程式碼物件的包名、類名和方法名),建立領域物件與程式碼物件的一一對映關係了。根據這種對映關係,相關人員可快速定位到業務邏輯所在的程式碼位置。在經過以上分析後,我們在微服務內就可以分析出如下圖的物件清單。
2. 設計微服務程式碼結構
應用層程式碼結構
應用層包括:應用服務、DTO 以及事件釋出相關程式碼。在 LeaveApplicationService 類內實現與聚合相關的應用服務,在 LoginApplicationService 封裝外部微服務認證和許可權的應用服務。
領域層程式碼結構
領域層包括一個或多個聚合的實體類、事件實體類、領域服務以及工廠、倉儲相關程式碼。一個聚合對應一個聚合程式碼目錄,聚合之間在程式碼上完全隔離,聚合之間通過應用層協調。
請假微服務領域層包含請假和人員兩個聚合。人員和請假程式碼都放在各自的聚合所在目錄結構的程式碼包中。如果隨著業務發展,人員相關功能需要從請假微服務中拆分出來,我們只需將人員聚合程式碼包稍加改造,獨立部署,即可快速釋出為人員微服務。到這裡,微服務內的領域物件,分層以及依賴關係就梳理清晰了。微服務的總體架構和程式碼模型也基本搭建完成了。
後續的工作
1. 詳細設計
在完成領域模型和微服務設計後,我們還需要對微服務進行詳細的設計。主要設計以下內容:實體屬性、資料庫表和欄位、實體與資料庫表對映、服務引數規約及功能實現等。
2. 程式碼開發和測試
開發人員只需要按照詳細的設計文件和功能要求,找到業務功能對應的程式碼位置,完成程式碼開發就可以了。程式碼開發完成後,開發人員要編寫單元測試用例,基於擋板模擬依賴物件完成服務測試。
基於DDD的微服務設計例項程式碼詳解
上一節用事件風暴完成的“線上請假考勤”專案的領域建模和微服務設計,接下來我們在這個專案的基礎上看看,用 DDD 方法設計和開發出來的微服務程式碼。點選 https://github.com/ouchuangxin/leave-sample 獲取完整程式碼
專案回顧
“線上請假考勤”專案中,請假的核心業務流程是:請假人填寫請假單提交審批;根據請假人身份、請假型別和請假天數進行校驗並確定審批規則;根據審批規則確定審批人,逐級提交上級審批,逐級核批通過則完成審批,否則審批不通過則退回申請人。
請假微服務採用的 DDD 設計思想
聚合中的物件
請假微服務包含請假(leave)、人員(person)和審批規則(rule)三個聚合。leave 聚合完成請假申請和稽核核心邏輯;person 聚合管理人員資訊和上下級關係;rule 是一個單實體聚合,提供請假審批規則查詢。
Leave 是請假微服務的核心聚合,它有請假單聚合根 leave、審批意見實體 ApprovalInfo、請假申請人 Applicant 和審批人 Approver 值物件(它們的資料來源於 person 聚合),還有部分列舉型別,如請假型別 LeaveType,請假單狀態 Status 和審批狀態型別 ApprovalType 等值物件。
下面我們通過程式碼來了解一下聚合根、實體以及值物件之間的關係。
1. 聚合根
聚合根 leave 中有屬性、值物件、關聯實體和自身的業務行為。Leave 實體採用充血模型,有自己的業務行為,具體就是聚合根實體類的方法,如程式碼中的 getDuration 和 addHistoryApprovalInfo 等方法。
聚合根引用實體和值物件,它可以組合聚合內的多個實體,在聚合根實體類方法中完成複雜的業務行為,這種複雜的業務行為也可以在聚合領域服務裡實現。但為了職責和邊界清晰,我建議聚合要根據自身的業務行為在實體類方法中實現,而涉及多個實體組合才能實現的業務能力由領域服務完成。
下面是聚合根 leave 的實體類方法,它包含屬性、對實體和值物件的引用以及自己的業務行為和方法。
public class Leave {
String id;
Applicant applicant;
Approver approver;
LeaveType type;
Status status;
Date startTime;
Date endTime;
long duration;
int leaderMaxLevel; //審批領導的最高階別
ApprovalInfo currentApprovalInfo;
List<ApprovalInfo> historyApprovalInfos;
public long getDuration() {
return endTime.getTime() - startTime.getTime();
}
public Leave addHistoryApprovalInfo(ApprovalInfo approvalInfo) {
if (null == historyApprovalInfos)
historyApprovalInfos = new ArrayList<>();
this.historyApprovalInfos.add(approvalInfo);
return this;
}
public Leave create(){
this.setStatus(Status.APPROVING);
this.setStartTime(new Date());
return this;
}
//其它方法
}
2. 實體
審批意見實體 ApprovalInfo 被 leave 聚合根引用,用於記錄審批意見,它有自己的屬性和值物件,如 approver 等,業務邏輯相對簡單。
public class ApprovalInfo {
String approvalInfoId;
Approver approver;
ApprovalType approvalType;
String msg;
long time;
}
3. 值物件
審批人值物件 Approver。這類值物件除了屬性集之外,還可以有簡單的資料查詢和轉換服務。Approver 資料來源於 person 聚合,從 person 聚合獲取審批人返回後,從 person 實體獲取 personID、personName 和 level 等屬性,重新組合為 approver 值物件,因此需要資料轉換和重新賦值。
Approver 值物件同時被聚合根 leave 和實體 approvalInfo 引用。這類值物件的資料來源於其它聚合,不可修改,可重複使用。將這種物件設計為值物件而不是實體,可以提高系統效能,降低資料庫實體關聯的複雜度,所以我一般建議優先設計為值物件。
public class Approver {
String personId;
String personName;
int level; //管理級別
public static Approver fromPerson(Person person){
Approver approver = new Approver();
approver.setPersonId(person.getPersonId());
approver.setPersonName(person.getPersonName());
approver.setLevel(person.getRoleLevel());
return approver;
}
}
列舉型別的值物件 Status 的程式碼
public enum Status {
APPROVING, APPROVED, REJECTED
}
由於值物件只做整體替換、不可修改的特性,在值物件中基本不會有修改或新增的方法
4. 領域服務
如果一個業務行為由多個實體物件參與完成,我們就將這部分業務邏輯放在領域服務中實現。領域服務與實體方法的區別是:實體方法完成單一實體自身的業務邏輯,是相對簡單的原子業務邏輯,而領域服務則是多個實體組合出的相對複雜的業務邏輯。兩者都在領域層,實現領域模型的核心業務能力。
一個聚合可以設計一個領域服務類,管理聚合內所有的領域服務。
請假聚合的領域服務類是 LeaveDomainService。領域服務中會用到很多的 DDD 設計模式,比如:用工廠模式實現複雜聚合的實體資料初始化,用倉儲模式實現領域層與基礎層的依賴倒置和用領域事件實現資料的最終一致性等。
public class LeaveDomainService {
@Autowired
EventPublisher eventPublisher;
@Autowired
LeaveRepositoryInterface leaveRepositoryInterface;
@Autowired
LeaveFactory leaveFactory;
@Transactional
public void createLeave(Leave leave, int leaderMaxLevel, Approver approver) {
leave.setLeaderMaxLevel(leaderMaxLevel);
leave.setApprover(approver);
leave.create();
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
LeaveEvent event = LeaveEvent.create(LeaveEventType.CREATE_EVENT, leave);
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));
eventPublisher.publish(event);
}
@Transactional
public void updateLeaveInfo(Leave leave) {
LeavePO po = leaveRepositoryInterface.findById(leave.getId());
if (null == po) {
throw new RuntimeException("leave does not exist");
}
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
}
@Transactional
public void submitApproval(Leave leave, Approver approver) {
LeaveEvent event;
if (ApprovalType.REJECT == leave.getCurrentApprovalInfo().getApprovalType()) {
leave.reject(approver);
event = LeaveEvent.create(LeaveEventType.REJECT_EVENT, leave);
} else {
if (approver != null) {
leave.agree(approver);
event = LeaveEvent.create(LeaveEventType.AGREE_EVENT, leave); } else {
leave.finish();
event = LeaveEvent.create(LeaveEventType.APPROVED_EVENT, leave);
}
}
leave.addHistoryApprovalInfo(leave.getCurrentApprovalInfo());
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));
eventPublisher.publish(event);
}
public Leave getLeaveInfo(String leaveId) {
LeavePO leavePO = leaveRepositoryInterface.findById(leaveId);
return leaveFactory.getLeave(leavePO);
}
public List<Leave> queryLeaveInfosByApplicant(String applicantId) {
List<LeavePO> leavePOList = leaveRepositoryInterface.queryByApplicantId(applicantId);
return leavePOList.stream().map(leavePO -> leaveFactory.getLeave(leavePO)).collect(Collectors.toList());
}
public List<Leave> queryLeaveInfosByApprover(String approverId) {
List<LeavePO> leavePOList = leaveRepositoryInterface.queryByApproverId(approverId);
return leavePOList.stream().map(leavePO -> leaveFactory.getLeave(leavePO)).collect(Collectors.toList());
}
}
領域服務開發時的注意事項:
在領域服務或實體方法中,我們應儘量避免呼叫其它聚合的領域服務或引用其它聚合的實體或值物件,這種操作會增加聚合的耦合度。在微服務架構演進時,如果出現聚合拆分和重組,這種跨聚合的服務呼叫和物件引用,會變成跨微服務的操作,導致這種跨聚合的領域服務呼叫和物件引用失效,在聚合分拆時會增加你程式碼解耦和重構的工作量。
以下是一段不建議使用的程式碼。在這段程式碼裡 Approver 是 leave 聚合的值物件,它作為物件引數被傳到 person 聚合的 findNextApprover 領域服務。如果在同一個微服務內,這種方式是沒有問題的。但在架構演進時,如果 person 和 leave 兩個聚合被分拆到不同的微服務中,那麼 leave 中的 Approver 物件以及它的 getPersonId() 和 fromPersonPO 方法在 person 聚合中就會失效,這時你就需要進行程式碼重構了。
public class PersonDomainService {
public Approver findNextApprover(Approver currentApprover, int leaderMaxLevel) {
PersonPO leaderPO = personRepository.findLeaderByPersonId(currentApprover.getPersonId());
if (leaderPO.getRoleLevel() > leaderMaxLevel) {
return null;
} else {
return Approver.fromPersonPO(leaderPO);
}
}
}
那正確的方式是什麼樣的呢?在應用服務組合不同聚合的領域服務時,我們可以通過 ID 或者引數來傳數,如單一引數 currentApproverId。這樣聚合之間就解耦了,下面是修改後的程式碼,它可以不依賴其它聚合的實體,獨立完成業務邏輯。
public class PersonDomainService {
public Person findNextApprover(String currentApproverId, int leaderMaxLevel) {
PersonPO leaderPO = personRepository.findLeaderByPersonId(currentApproverId);
if (leaderPO.getRoleLevel() > leaderMaxLevel) {
return null;
} else {
return personFactory.createPerson(leaderPO);
}
}
}
領域事件
在建立請假單和請假審批過程中會產生領域事件。為了方便管理,我們將聚合內的領域事件相關的程式碼放在聚合的 event 目錄中。領域事件實體在聚合倉儲內完成持久化,但是事件實體的生命週期不受聚合根管理。
1. 領域事件基類 DomainEvent
你可以建立統一的領域事件基類 DomainEvent。基類包含:事件 ID、時間戳、事件源以及事件相關的業務資料。
public class DomainEvent {
String id;
Date timestamp;
String source;
String data;
}
2. 領域事件實體
請假領域事件實體 LeaveEvent 繼承基類 DomainEvent。可根據需要擴充套件屬性和方法,如 leaveEventType。data 欄位中儲存領域事件相關的業務資料,可以是 XML 或 Json 等格式。
public class LeaveEvent extends DomainEvent {
LeaveEventType leaveEventType;
public static LeaveEvent create(LeaveEventType eventType, Leave leave){
LeaveEvent event = new LeaveEvent();
event.setId(IdGenerator.nextId());
event.setLeaveEventType(eventType);
event.setTimestamp(new Date());
event.setData(JSON.toJSONString(leave));
return event;
}
}
3. 領域事件的執行邏輯
一般來說,領域事件的執行邏輯如下:
第一步:執行業務邏輯,產生領域事件。
第二步:完成業務資料持久化。
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
第三步:完成事件資料持久化。
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event));
第四步:完成領域事件釋出。
eventPublisher.publish(event);
以上領域事件處理邏輯程式碼詳見 LeaveDomainService 中 submitApproval 領域服務,裡面有請假提交審批事件的完整處理邏輯。
4. 領域事件資料持久化
為了保證事件釋出方與事件訂閱方資料的最終一致性和資料審計,有些業務場景需要建立資料對賬機制。資料對賬主要通過對源端和目的端的持久化資料比對,從而發現異常資料並進一步處理,保證資料最終一致性。
對於需要對賬的事件資料,我們需設計領域事件物件的持久化物件 PO,完成領域事件資料的持久化,如 LeaveEvent 事件實體的持久化物件 LeaveEventPO。再通過聚合的倉儲完成資料持久化:
leaveRepositoryInterface.saveEvent(leaveFactory.createLeaveEventPO(event))。
事件資料持久化物件 LeaveEventPO 格式如下:
public class LeaveEventPO {
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
int id;
@Enumerated(EnumType.STRING)
LeaveEventType leaveEventType;
Date timestamp;
String source;
String data;
}
倉儲模式
領域模型中 DO 實體的資料持久化是必不可少的,DDD 採用倉儲模式實現資料持久化,使得業務邏輯與基礎資源邏輯解耦,實現依賴倒置。持久化時先完成 DO 與 PO 物件的轉換,然後在倉儲服務中完成 PO 物件的持久化。
1. DO 與 PO 物件的轉換
Leave 聚合根的 DO 實體除了自身的屬性外,還會根據領域模型引用多個值物件,如 Applicant 和 Approver 等,它們包含多個屬性,如:personId、personName 和 personType 等屬性。
在持久化物件 PO 設計時,你可以將這些值物件屬性嵌入 PO 屬性中,或設計一個組合屬性欄位,以 Json 串的方式儲存在 PO 中。
以下是 leave 的 DO 的屬性定義:
public class Leave {
String id;
Applicant applicant;
Approver approver;
LeaveType type;
Status status;
Date startTime;
Date endTime;
long duration;
int leaderMaxLevel;
ApprovalInfo currentApprovalInfo;
List<ApprovalInfo> historyApprovalInfos;
}
public class Applicant {
String personId;
String personName;
String personType;
}
public class Approver {
String personId;
String personName;
int level;
}
為了減少資料庫表數量以及表與表的複雜關聯關係,我們將 leave 實體和多個值物件放在一個 LeavePO 中。如果以屬性嵌入的方式,Applicant 值物件在 LeavePO 中會展開為:applicantId、applicantName 和 applicantType 三個屬性。
以下為採用屬性嵌入方式的持久化物件 LeavePO 的結構。
public class LeavePO {
@Id
@GenericGenerator(name="idGenerator", strategy="uuid")
@GeneratedValue(generator="idGenerator")
String id;
String applicantId;
String applicantName;
@Enumerated(EnumType.STRING)
PersonType applicantType;
String approverId;
String approverName;
@Enumerated(EnumType.STRING)
LeaveType leaveType;
@Enumerated(EnumType.STRING)
Status status;
Date startTime;
Date endTime;
long duration;
@Transient
List<ApprovalInfoPO> historyApprovalInfoPOList;
}
2. 倉儲模式
為了解耦業務邏輯和基礎資源,我們可以在基礎層和領域層之間增加一層倉儲服務,實現依賴倒置。通過這一層可以實現業務邏輯和基礎層資源的依賴分離。在變更基礎層資料庫的時候,你只要替換倉儲實現就可以了,上層核心業務邏輯不會受基礎資源變更的影響,從而實現依賴倒置。
一個聚合一個倉儲,實現聚合資料的持久化。領域服務通過倉儲介面來訪問基礎資源,由倉儲實現完成資料持久化和初始化。倉儲一般包含:倉儲介面和倉儲實現。
2.1 倉儲介面
倉儲介面面向領域服務提供介面。
public interface LeaveRepositoryInterface {
void save(LeavePO leavePO);
void saveEvent(LeaveEventPO leaveEventPO);
LeavePO findById(String id);
List<LeavePO> queryByApplicantId(String applicantId);
List<LeavePO> queryByApproverId(String approverId);
}
2.2 倉儲實現
倉儲實現完成資料持久化和資料庫查詢。
@Repository
public class LeaveRepositoryImpl implements LeaveRepositoryInterface {
@Autowired
LeaveDao leaveDao;
@Autowired
ApprovalInfoDao approvalInfoDao;
@Autowired
LeaveEventDao leaveEventDao;
public void save(LeavePO leavePO) {
leaveDao.save(leavePO);
approvalInfoDao.saveAll(leavePO.getHistoryApprovalInfoPOList());
}
public void saveEvent(LeaveEventPO leaveEventPO){
leaveEventDao.save(leaveEventPO);
}
@Override
public LeavePO findById(String id) {
return leaveDao.findById(id)
.orElseThrow(() -> new RuntimeException("leave not found"));
}
@Override
public List<LeavePO> queryByApplicantId(String applicantId) {
List<LeavePO> leavePOList = leaveDao.queryByApplicantId(applicantId);
leavePOList.stream()
.forEach(leavePO -> {
List<ApprovalInfoPO> approvalInfoPOList = approvalInfoDao.queryByLeaveId(leavePO.getId());
leavePO.setHistoryApprovalInfoPOList(approvalInfoPOList);
});
return leavePOList;
}
@Override
public List<LeavePO> queryByApproverId(String approverId) {
List<LeavePO> leavePOList = leaveDao.queryByApproverId(approverId);
leavePOList.stream()
.forEach(leavePO -> {
List<ApprovalInfoPO> approvalInfoPOList = approvalInfoDao.queryByLeaveId(leavePO.getId());
leavePO.setHistoryApprovalInfoPOList(approvalInfoPOList);
});
return leavePOList;
}
}
這裡持久化元件採用了 Jpa。
public interface LeaveDao extends JpaRepository<LeavePO, String> {
List<LeavePO> queryByApplicantId(String applicantId);
List<LeavePO> queryByApproverId(String approverId);
}
2.3 倉儲執行邏輯
以建立請假單為例,倉儲的執行步驟如下。
第一步:倉儲執行之前將聚合內 DO 會轉換為 PO,這種轉換在工廠服務中完成:
leaveFactory.createLeavePO(leave)。
第二步:完成物件轉換後,領域服務呼叫倉儲介面:
leaveRepositoryInterface.save。
第三步:由倉儲實現完成 PO 物件持久化。
程式碼執行步驟如下:
public void createLeave(Leave leave, int leaderMaxLevel, Approver approver) {
leave.setLeaderMaxLevel(leaderMaxLevel);
leave.setApprover(approver);
leave.create();
leaveRepositoryInterface.save(leaveFactory.createLeavePO(leave));
}
工廠模式
對於大型的複雜領域模型,聚合內的聚合根、實體和值物件之間的依賴關係比較複雜,這種過於複雜的依賴關係,不適合通過根實體構造器來建立。為了協調這種複雜的領域物件的建立和生命週期管理,在 DDD 裡引入了工廠模式(Factory),在工廠裡封裝複雜的物件建立過程。
當聚合根被建立時,聚合內所有依賴的物件將會被同時建立。
工廠與倉儲模式往往結對出現,應用於資料的初始化和持久化兩類場景。
- DO 物件的初始化:獲取持久化物件 PO,通過工廠一次構建出聚合根所有依賴的 DO 物件,完資料初始化。
- DO 的物件持久化:將所有依賴的 DO 物件一次轉換為 PO 物件,完成資料持久化。
下面程式碼是 leave 聚合的工廠類 LeaveFactory。其中 createLeavePO(leave)方法組織 leave 聚合的 DO 物件和值物件完成 leavePO 物件的構建。getLeave(leave)通過持久化物件 PO 構建聚合的 DO 物件和值物件,完成 leave 聚合 DO 實體的初始化。
public class LeaveFactory {
public LeavePO createLeavePO(Leave leave) {
LeavePO leavePO = new LeavePO();
leavePO.setId(UUID.randomUUID().toString());
leavePO.setApplicantId(leave.getApplicant().getPersonId());
leavePO.setApplicantName(leave.getApplicant().getPersonName());
leavePO.setApproverId(leave.getApprover().getPersonId());
leavePO.setApproverName(leave.getApprover().getPersonName());
leavePO.setStartTime(leave.getStartTime());
leavePO.setStatus(leave.getStatus());
List<ApprovalInfoPO> historyApprovalInfoPOList = approvalInfoPOListFromDO(leave);
leavePO.setHistoryApprovalInfoPOList(historyApprovalInfoPOList);
return leavePO;
}
public Leave getLeave(LeavePO leavePO) {
Leave leave = new Leave();
Applicant applicant = Applicant.builder()
.personId(leavePO.getApplicantId())
.personName(leavePO.getApplicantName())
.build();
leave.setApplicant(applicant);
Approver approver = Approver.builder()
.personId(leavePO.getApproverId())
.personName(leavePO.getApproverName())
.build();
leave.setApprover(approver);
leave.setStartTime(leavePO.getStartTime());
leave.setStatus(leavePO.getStatus());
List<ApprovalInfo> approvalInfos = getApprovalInfos(leavePO.getHistoryApprovalInfoPOList());
leave.setHistoryApprovalInfos(approvalInfos);
return leave;
}
//其它方法
}
服務的組合與編排
應用層的應用服務完成領域服務的組合與編排。一個聚合的應用服務可以建立一個應用服務類,管理聚合所有的應用服務。比如 leave 聚合有 LeaveApplicationService,person 聚合有 PersonApplicationService。
在請假微服務中,有三個聚合:leave、person 和 rule。我們來看一下應用服務是如何跨聚合來進行服務的組合和編排的。以建立請假單 createLeaveInfo 應用服務為例,分為這樣三個步驟。
第一步:根據請假單定義的人員型別、請假型別和請假時長從 rule 聚合中獲取請假審批規則。這一步通過 approvalRuleDomainService 類的 getLeaderMaxLevel 領域服務來實現。
第二步:根據請假審批規則,從 person 聚合中獲取請假審批人。這一步通過 personDomainService 類的 findFirstApprover 領域服務來實現。
第三步:根據請假資料和從 rule 和 person 聚合獲取的資料,建立請假單。這一步通過 leaveDomainService 類的 createLeave 領域服務來實現。
由於領域核心邏輯已經很好地沉澱到了領域層中,領域層的這些核心邏輯可以高度複用。應用服務只需要靈活地組合和編排這些不同聚合的領域服務,就可以很容易地適配前端業務的變化。因此應用層不會積累太多的業務邏輯程式碼,所以會變得很薄,程式碼維護起來也會容易得多。
以下是 leave 聚合的應用服務類。
public class LeaveApplicationService{
@Autowired
LeaveDomainService leaveDomainService;
@Autowired
PersonDomainService personDomainService;
@Autowired
ApprovalRuleDomainService approvalRuleDomainService;
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}
public void updateLeaveInfo(Leave leave){
leaveDomainService.updateLeaveInfo(leave);
}
public void submitApproval(Leave leave){
//find next approver
Person approver = personDomainService.findNextApprover(leave.getApprover().getPersonId(), leave.getLeaderMaxLevel());
leaveDomainService.submitApproval(leave, Approver.fromPerson(approver));
}
public Leave getLeaveInfo(String leaveId){
return leaveDomainService.getLeaveInfo(leaveId);
}
public List<Leave> queryLeaveInfosByApplicant(String applicantId){
return leaveDomainService.queryLeaveInfosByApplicant(applicantId);
}
public List<Leave> queryLeaveInfosByApprover(String approverId){
return leaveDomainService.queryLeaveInfosByApprover(approverId);
}
}
應用服務開發注意事項:
為了聚合解耦和微服務架構演進,應用服務在對不同聚合領域服務進行編排時,應避免不同聚合的實體物件,在不同聚合的領域服務中引用,這是因為一旦聚合拆分和重組,這些跨聚合的物件將會失效。
在 LeaveApplicationService 中,leave 實體和 Applicant 值物件分別作為引數被 rule 聚合和 person 聚合的領域服務引用,這樣會增加聚合的耦合度。下面是不推薦使用的程式碼。
public class LeaveApplicationService{
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
ApprovalRule rule = approvalRuleDomainService.getLeaveApprovalRule(leave);
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(rule);
leave.setLeaderMaxLevel(leaderMaxLevel);
//find next approver
Approver approver = personDomainService.findFirstApprover(leave.getApplicant(), leaderMaxLevel);
leave.setApprover(approver);
leaveDomainService.createLeave(leave);
}
}
那如何實現聚合的解耦呢?我們可以將跨聚合呼叫時的物件傳值調整為引數傳值。一起來看一下調整後的程式碼,getLeaderMaxLevel 由 leave 物件傳值調整為 personType,leaveType 和 duration 引數傳值。findFirstApprover 中 Applicant 值物件調整為 personId 引數傳值。
public class LeaveApplicationService{
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}
}
在微服務演進和聚合重組時,就不需要進行聚合解耦和程式碼重構了。
微服務聚合拆分時的程式碼演進
如果請假微服務未來需要演進為人員和請假兩個微服務,我們可以基於請假 leave 和人員 person 兩個聚合來進行拆分。由於兩個聚合已經完全解耦,領域邏輯非常穩定,在微服務聚合程式碼拆分時,聚合領域層的程式碼基本不需要調整。調整主要集中在微服務的應用服務中。
我們以應用服務 createLeaveInfo 為例,當一個微服務拆分為兩個微服務時,看看程式碼需要做什麼樣的調整?
1. 微服務拆分前
createLeaveInfo 應用服務的程式碼如下:
public void createLeaveInfo(Leave leave){
//get approval leader max level by rule
int leaderMaxLevel = approvalRuleDomainService.getLeaderMaxLevel(leave.getApplicant().getPersonType(), leave.getType().toString(), leave.getDuration());
//find next approver
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
leaveDomainService.createLeave(leave, leaderMaxLevel, Approver.fromPerson(approver));
}
2. 微服務拆分後
leave 和 person 兩個聚合隨微服務拆分後,createLeaveInfo 應用服務中下面的程式碼將會變成跨微服務呼叫。
Person approver = personDomainService.findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
由於跨微服務的呼叫是在應用層完成的,我們只需要調整 createLeaveInfo 應用服務程式碼,將原來微服務內的服務呼叫 personDomainService.findFirstApprover 修改為跨微服務的服務呼叫:personFeignService. findFirstApprover。
同時新增 ApproverAssembler 組裝器和 PersonResponse 的 DTO 物件,以便將 person 微服務返回的 person DTO 物件轉換為 approver 值物件。
// PersonResponse為呼叫微服務返回結果的封裝
//通過personFeignService呼叫Person微服務使用者介面層的findFirstApprover facade介面
PersonResponse approverResponse = personFeignService. findFirstApprover(leave.getApplicant().getPersonId(), leaderMaxLevel);
Approver approver = ApproverAssembler.toDO(approverResponse);
在原來的 person 聚合中,由於 findFirstApprover 領域服務已經逐層封裝為使用者介面層的 Facade 介面,所以 person 微服務不需要做任何程式碼調整,只需將 PersonApi 的 findFirstApprover Facade 服務,釋出到 API 閘道器即可。
如果拆分前 person 聚合的 findFirstApprover 領域服務,沒有被封裝為 Facade 介面,我們只需要在 person 微服務中按照以下步驟調整即可。
第一步:將 person 聚合 PersonDomainService 類中的領域服務 findFirstApprover 封裝為應用服務 findFirstApprover。
@Service
public class PersonApplicationService {
@Autowired
PersonDomainService personDomainService;
public Person findFirstApprover(String applicantId, int leaderMaxLevel) {
return personDomainService.findFirstApprover(applicantId, leaderMaxLevel);
}
}
第二步:將應用服務封裝為 Facade 服務,併發布到 API 閘道器。
@RestController
@RequestMapping("/person")
@Slf4j
public class PersonApi {
@Autowired
@GetMapping("/findFirstApprover")
public Response findFirstApprover(@RequestParam String applicantId, @RequestParam int leaderMaxLevel) {
Person person = personApplicationService.findFirstApprover(applicantId, leaderMaxLevel);
return Response.ok(PersonAssembler.toDTO(person));
}
}
服務介面的提供
使用者介面層是前端應用與微服務應用層的橋樑,通過 Facade 介面封裝應用服務,適配前端並提供靈活的服務,完成 DO 和 DTO 相互轉換。
當應用服務接收到前端請求資料時,組裝器會將 DTO 轉換為 DO。當應用服務向前端返回資料時,組裝器會將 DO 轉換為 DTO。
1. facade 介面
facade 介面可以是一個門面介面實現類,也可以是門面介面加一個門面介面實現類。專案可以根據前端的複雜度進行選擇,由於請假微服務前端功能相對簡單,我們就直接用一個門面介面實現類來實現就可以了。
public class LeaveApi {
@PostMapping
public Response createLeaveInfo(LeaveDTO leaveDTO){
Leave leave = LeaveAssembler.toDO(leaveDTO);
leaveApplicationService.createLeaveInfo(leave);
return Response.ok();
}
@PostMapping("/query/applicant/{applicantId}")
public Response queryByApplicant(@PathVariable String applicantId){
List<Leave> leaveList = leaveApplicationService.queryLeaveInfosByApplicant(applicantId);
List<LeaveDTO> leaveDTOList = leaveList.stream().map(leave -> LeaveAssembler.toDTO(leave)).collect(Collectors.toList());
return Response.ok(leaveDTOList);
}
//其它方法
}
2. DTO 資料組裝
組裝類(Assembler):負責將應用服務返回的多個 DO 物件組裝為前端 DTO 物件,或將前端請求的 DTO 物件轉換為多個 DO 物件,供應用服務作為引數使用。組裝類中不應有業務邏輯,主要負責格式轉換、欄位對映等。Assembler 往往與 DTO 同時存在。LeaveAssembler 完成請假 DO 和 DTO 資料相互轉換。
public class LeaveAssembler {
public static LeaveDTO toDTO(Leave leave){
LeaveDTO dto = new LeaveDTO();
dto.setLeaveId(leave.getId());
dto.setLeaveType(leave.getType().toString());
dto.setStatus(leave.getStatus().toString());
dto.setStartTime(DateUtil.formatDateTime(leave.getStartTime()));
dto.setEndTime(DateUtil.formatDateTime(leave.getEndTime()));
dto.setCurrentApprovalInfoDTO(ApprovalInfoAssembler.toDTO(leave.getCurrentApprovalInfo()));
List<ApprovalInfoDTO> historyApprovalInfoDTOList = leave.getHistoryApprovalInfos()
.stream()
.map(historyApprovalInfo -> ApprovalInfoAssembler.toDTO(leave.getCurrentApprovalInfo()))
.collect(Collectors.toList());
dto.setHistoryApprovalInfoDTOList(historyApprovalInfoDTOList);
dto.setDuration(leave.getDuration());
return dto;
}
public static Leave toDO(LeaveDTO dto){
Leave leave = new Leave();
leave.setId(dto.getLeaveId());
leave.setApplicant(ApplicantAssembler.toDO(dto.getApplicantDTO()));
leave.setApprover(ApproverAssembler.toDO(dto.getApproverDTO()));
leave.setCurrentApprovalInfo(ApprovalInfoAssembler.toDO(dto.getCurrentApprovalInfoDTO()));
List<ApprovalInfo> historyApprovalInfoDTOList = dto.getHistoryApprovalInfoDTOList()
.stream()
.map(historyApprovalInfoDTO -> ApprovalInfoAssembler.toDO(historyApprovalInfoDTO))
.collect(Collectors.toList());
leave.setHistoryApprovalInfos(historyApprovalInfoDTOList);
return leave;
}
}
DTO 類:包括 requestDTO 和 responseDTO 兩部分。
DTO 應儘量根據前端展示資料的需求來定義,避免過多地暴露後端業務邏輯。尤其對於多渠道場景,可以根據渠道屬性和要求,為每個渠道前端應用定義個性化的 DTO。由於請假微服務相對簡單,我們可以用 leaveDTO 程式碼做個示例。
@Data
public class LeaveDTO {
String leaveId;
ApplicantDTO applicantDTO;
ApproverDTO approverDTO;
String leaveType;
ApprovalInfoDTO currentApprovalInfoDTO;
List<ApprovalInfoDTO> historyApprovalInfoDTOList;
String startTime;
String endTime;
long duration;
String status;
}
總結
聚合與聚合的解耦:當多個聚合在同一個微服務時,很多傳統架構開發人員會下意識地引用其他聚合的實體和值物件,或者呼叫其它聚合的領域服務。因為這些聚合的程式碼在同一個微服務內,執行時不會有問題,開發效率似乎也更高,但這樣會不自覺地增加聚合之間的耦合。在微服務架構演進時,如果聚合被分別拆分到不同的微服務中,原來微服務內的關係就會變成跨微服務的關係,原來微服務內的物件引用或服務呼叫將會失效。最終你還是免不了要花大量的精力去做聚合解耦。雖然前期領域建模和邊界劃分得很好,但可能會因為開發稍不注意,而導致解耦工作前功盡棄。
微服務內各層的解耦:微服務內有四層,在應用層和領域層組成核心業務領域的兩端,有兩個緩衝區或資料轉換區。前端與應用層通過組裝器實現 DTO 和 DO 的轉換,這種適配方式可以更容易地響應前端需求的變化,隱藏核心業務邏輯的實現,保證核心業務邏輯的穩定,實現核心業務邏輯與前端應用的解耦。而領域層與基礎層通過倉儲和工廠模式實現 DO 和 PO 的轉換,實現應用邏輯與基礎資源邏輯的解耦。
總結(一):微服務設計和拆分要堅持哪些原則?
微服務的演進策略
在從單體向微服務演進時,演進策略大體分為兩種:絞殺者策略和修繕者策略。
1. 絞殺者策略
絞殺者策略是一種逐步剝離業務能力,用微服務逐步替代原有單體系統的策略。它對單體系統進行領域建模,根據領域邊界,在單體系統之外,將新功能和部分業務能力獨立出來,建設獨立的微服務。新微服務與單體系統保持鬆耦合關係。
隨著時間的推移,大部分單體系統的功能將被獨立為微服務,這樣就慢慢絞殺掉了原來的單體系統。絞殺者策略類似建築拆遷,完成部分新建築物後,然後拆除部分舊建築物。
2. 修繕者策略
修繕者策略是一種維持原有系統整體能力不變,逐步優化系統整體能力的策略。它是在現有系統的基礎上,剝離影響整體業務的部分功能,獨立為微服務,比如高效能要求的功能,程式碼質量不高或者版本釋出頻率不一致的功能等。
通過這些功能的剝離,我們就可以兼顧整體和區域性,解決系統整體不協調的問題。修繕者策略類似古建築修復,將存在問題的部分功能重建或者修復後,重新加入到原有的建築中,保持建築原貌和功能不變。一般人從外表感覺不到這個變化,但是建築物質量卻得到了很大的提升。
其實還有第三種策略,就是另起爐灶,顧名思義就是將原有的系統推倒重做。建設期間,原有單體系統照常執行,一般會停止開發新需求。而新系統則會組織新的專案團隊,按照原有系統的功能域,重新做領域建模,開發新的微服務。在完成資料遷移後,進行新舊系統切換。
不同場景下的領域建模策略
1. 新建系統
新建系統又分為簡單和複雜領域建模兩種場景。
簡單領域建模
簡單的業務領域,一個領域就是一個小的子域。在這個小的問題域內,領域建模過程相對簡單,直接採用事件風暴的方法構建領域模型就可以了。
複雜領域建模
對於複雜的業務領域,領域可能需要多級拆分後才能開始領域建模。領域拆分為子域,甚至子域還需要進一步拆分。
對於複雜領域,我們可以分三步來完成領域建模和微服務設計。
第一步,拆分子域建立領域模型
第二步,領域模型微調
第三步,微服務的設計和拆分
2. 單體遺留系統
如果我們面對的是一個單體遺留系統,只需要將部分功能獨立為微服務,而其餘仍為單體,整體保持不變,比如將面臨效能瓶頸的模組拆分為微服務。我們只需要將這一特定功能,理解為一個簡單子領域,參考簡單領域建模的方式就可以了。在微服務設計中,我們還要考慮新老系統之間服務和業務的相容,必要時可引入防腐層。
DDD 使用的誤區
很多人在接觸微服務後,但凡是系統,一概都想設計成微服務架構。其實有些業務場景,單體架構的開發成本會更低,開發效率更高,採用單體架構也不失為好的選擇。同樣,雖然 DDD 很好,但有些傳統設計方法在微服務設計時依然有它的用武之地。下面我們就來聊聊 DDD 使用的幾個誤區。
1. 所有的領域都用 DDD
很多人在學會 DDD 後,可能會將其用在所有業務域,即全部使用 DDD 來設計。DDD 從戰略設計到戰術設計,是一個相對複雜的過程,首先企業內要培養 DDD 的文化,其次對團隊成員的設計和技術能力要求相對比較高。在資源有限的情況下,應聚焦核心域,建議你先從富領域模型的核心域開始,而不必一下就在全業務域推開。
2. 全部採用 DDD 戰術設計方法
不同的設計方法有它的適用環境,我們應選擇它最擅長的場景。DDD 有很多的概念和戰術設計方法,比如聚合根和值物件等。聚合根利用倉儲管理聚合內實體資料之間的一致性,這種方法對於管理新建和修改資料非常有效,比如在修改訂單資料時,它可以保證訂單總金額與所有商品明細金額的一致,但它並不擅長較大資料量的查詢處理,甚至有延遲載入進而影響效率的問題。
而傳統的設計方法,可能一條簡單的 SQL 語句就可以很快地解決問題。而很多貧領域模型的業務,比如資料統計和分析,DDD 很多方法可能都用不上,或用得並不順手,而傳統的方法很容易就解決了。
因此,在遵守領域邊界和微服務分層等大原則下,在進行戰術層面設計時,我們應該選擇最適合的方法,不只是 DDD 設計方法,當然還應該包括傳統的設計方法。這裡要以快速、高效解決實際問題為最佳,不要為做 DDD 而做 DDD。
3. 重戰術設計而輕戰略設計
戰略設計時構建的領域模型,是微服務設計和開發的輸入,它確定了微服務的邊界、聚合、程式碼物件以及服務等關鍵領域物件。領域模型邊界劃分得清不清晰,領域物件定義得明不明確,會決定微服務的設計和開發質量。沒有領域模型的輸入,基於 DDD 的微服務的設計和開發將無從談起。因此我們不僅要重視戰術設計,更要重視戰略設計。
4. DDD 只適用於微服務
DDD 是在微服務出現後才真正火爆起來的,很多人會認為 DDD 只適用於微服務。在 DDD 沉默的二十多年裡,其實它一直也被應用在單體應用的設計中。
具體專案實施時,要吸取 DDD 的核心設計思想和理念,結合具體的業務場景和團隊技術特點,多種方法組合,靈活運用,用正確的方式解決實際問題。
微服務設計原則
第一條:要領域驅動設計,而不是資料驅動設計,也不是介面驅動設計。
微服務設計首先應建立領域模型,確定邏輯和物理邊界以及領域物件後,然後才開始微服務的拆分和設計。而不是先定義資料模型和庫表結構,也不是前端介面需要什麼,就去調整核心領域邏輯程式碼。在設計時應該將外部需求從外到內逐級消化,儘量降低對核心領域層邏輯的影響。
第二條:要邊界清晰的微服務,而不是泥球小單體。
微服務上線後其功能和程式碼也不是一成不變的。隨著需求或設計變化,領域模型會迭代,微服務的程式碼也會分分合合。邊界清晰的微服務,可快速實現微服務程式碼的重組。微服務內聚合之間的領域服務和資料庫實體原則上應杜絕相互依賴。你可通過應用服務編排或者事件驅動,實現聚合之間的解耦,以便微服務的架構演進。
第三條:要職能清晰的分層,而不是什麼都放的大籮筐。
分層架構中各層職能定位清晰,且都只能與其下方的層發生依賴,也就是說只能從外層呼叫內層服務,內層通過封裝、組合或編排對外逐層暴露,服務粒度也由細到粗。應用層負責服務的組合和編排,不應有太多的核心業務邏輯,領域層負責核心領域業務邏輯的實現。各層應各司其職,職責邊界不要混亂。在服務演進時,應儘量將可複用的能力向下層沉澱。
第四條:要做自己能 hold 住的微服務,而不是過度拆分的微服務。
微服務過度拆分必然會帶來軟體維護成本的上升,比如:整合成本、運維成本、監控和定位問題的成本。企業在微服務轉型過程中還需要有云計算、DevOps、自動化監控等能力,而一般企業很難在短時間內提升這些能力,如果專案團隊沒有這些能力,將很難 hold 住這些微服務。
微服務拆分需要考慮哪些因素?
1. 基於領域模型
基於領域模型進行拆分,圍繞業務領域按職責單一性、功能完整性拆分。
2. 基於業務需求變化頻率
識別領域模型中的業務需求變動頻繁的功能,考慮業務變更頻率與相關度,將業務需求變動較高和功能相對穩定的業務進行分離。這是因為需求的經常性變動必然會導致程式碼的頻繁修改和版本釋出,這種分離可以有效降低頻繁變動的敏態業務對穩態業務的影響。
3. 基於應用效能
識別領域模型中效能壓力較大的功能。因為效能要求高的功能可能會拖累其它功能,在資源要求上也會有區別,為了避免對整體效能和資源的影響,我們可以把在效能方面有較高要求的功能拆分出去。
4. 基於組織架構和團隊規模
除非有意識地優化組織架構,否則微服務的拆分應儘量避免帶來團隊和組織架構的調整,避免由於功能的重新劃分,而增加大量且不必要的團隊之間的溝通成本。拆分後的微服務專案團隊規模保持在 10~12 人左右為宜。
5. 基於安全邊界
有特殊安全要求的功能,應從領域模型中拆分獨立,避免相互影響。
6. 基於技術異構等因素
領域模型中有些功能雖然在同一個業務域內,但在技術實現時可能會存在較大的差異,也就是說領域模型內部不同的功能存在技術異構的問題。由於業務場景或者技術條件的限制,有的可能用.NET,有的則是 Java,有的甚至大資料架構。對於這些存在技術異構的功能,可以考慮按照技術邊界進行拆分。
總結(二):分散式架構關鍵設計10問
前面我們重點講述了領域建模、微服務設計和前端設計方法,它們組合在一起就可以形成中臺建設的整體解決方案。而中臺大多基於分散式微服務架構,這種企業級的數字化轉型有很多地方值得我們關注和思考。
我們不僅要關注企業商業模式、業務邊界以及前中臺的融合,還要關注資料技術體系、微服務設計、多活等多領域的設計和協同。結合實施經驗和思考,今天我們就來聊聊分散式架構下的幾個關鍵問題。
一、選擇什麼樣的分散式資料庫?
分散式架構下的資料應用場景遠比集中式架構複雜,會產生很多資料相關的問題。談到資料,首先就是要選擇合適的分散式資料庫。分散式資料庫大多采用資料多副本的方式,實現資料訪問的高效能、多活和容災。
目前主要有三種不同的分散式資料庫解決方案。它們的主要差異是資料多副本的處理方式和資料庫中介軟體。
1. 一體化分散式資料庫方案
它支援資料多副本、高可用。多采用 Paxos 協議,一次寫入多資料副本,多數副本寫入成功即算成功。代表產品是 OceanBase 和高斯資料庫。
2. 集中式資料庫 + 資料庫中介軟體方案
它是集中式資料庫與資料庫中介軟體結合的方案,通過資料庫中介軟體實現資料路由和全域性資料管理。資料庫中介軟體和資料庫獨立部署,採用資料庫自身的同步機制實現主副本資料的一致性。集中式資料庫主要有 MySQL 和 PostgreSQL 資料庫,基於這兩種資料庫衍生出了很多的解決方案,比如開源資料庫中介軟體 MyCat+MySQL 方案,TBase(基於 PostgreSQL,但做了比較大的封裝和改動)等方案。
3. 集中式資料庫 + 分庫類庫方案
它是一種輕量級的資料庫中介軟體方案,分庫類庫實際上是一個基礎 JAR 包,與應用軟體部署在一起,實現資料路由和資料歸集。它適合比較簡單的讀寫交易場景,在強一致性和聚合分析查詢方面相對較弱。典型分庫基礎元件有 ShardingSphere。
小結:這三種方案實施成本不一樣,業務支援能力差異也比較大。一體化分散式資料庫主要由網際網路大廠開發,具有超強的資料處理能力,大多需要雲端計算底座,實施成本和技術能力要求比較高。集中式資料庫 + 資料庫中介軟體方案,實施成本和技術能力要求適中,可滿足中大型企業業務要求。第三種分庫類庫的方案可處理簡單的業務場景,成本和技能要求相對較低。在選擇資料庫的時候,我們要考慮自身能力、成本以及業務需要,從而選擇合適的方案。
二、如何設計資料庫分庫主鍵?
與客戶接觸的關鍵業務,我建議你以客戶 ID 作為分庫主鍵。這樣可以確保同一個客戶的資料分佈在同一個資料單元內,避免出現跨資料單元的頻繁資料訪問。跨資料中心的頻繁服務呼叫或跨資料單元的查詢,會對系統效能造成致命的影響。
三、資料庫的資料同步和複製
在微服務架構中,資料被進一步分割。為了實現資料的整合,資料庫之間批量資料同步與複製是必不可少的。資料同步與複製主要用於資料庫之間的資料同步,實現業務資料遷移、資料備份、不同渠道核心業務資料向資料平臺或資料中臺的資料複製、以及不同主題資料的整合等。
傳統的資料傳輸方式有 ETL 工具和定時提數程式,但資料在時效性方面存在短板。分散式架構一般採用基於資料庫邏輯日誌增量資料捕獲(CDC)技術,它可以實現準實時的資料複製和傳輸,實現資料處理與應用邏輯解耦,使用起來更加簡單便捷。
四、跨庫關聯查詢如何處理?
跨庫關聯查詢是分散式資料庫的一個短板,會影響查詢效能。在領域建模時,很多實體會分散到不同的微服務中,但很多時候會因為業務需求,它們之間需要關聯查詢。
關聯查詢的業務場景包括兩類:第一類是基於某一維度或某一主題域的資料查詢,比如基於客戶全業務檢視的資料查詢,這種查詢會跨多個業務線的微服務;第二類是表與表之間的關聯查詢,比如機構表與業務表的聯表查詢,但機構表和業務表分散在不同的微服務。
如何解決這兩類關聯查詢呢?
對於第一類場景,由於資料分散在不同微服務裡,我們無法跨多個微服務來統計這些資料。你可以建立面向主題的分散式資料庫,它的資料來源於不同業務的微服務。採用資料庫日誌捕獲技術,從各業務端微服務將資料準實時彙集到主題資料庫。在資料彙集時,提前做好資料關聯(如將多表資料合併為一個寬表)或者建立資料模型。面向主題資料庫建設查詢微服務。這樣一次查詢你就可以獲取客戶所有維度的業務資料了。你還可以根據主題或場景設計合適的分庫主鍵,提高查詢效率。
對於第二類場景,對於不在同一個資料庫的表與表之間的關聯查詢場景,你可以採用小表廣播,在業務庫中增加一張冗餘的程式碼副表。當主表資料發生變化時,你可以通過訊息釋出和訂閱的領域事件驅動模式,非同步重新整理所有副表資料。這樣既可以解決表與表的關聯查詢,還可以提高資料的查詢效率。
五、如何處理高頻熱點資料?
常見的做法是將這些高頻熱點資料,從資料庫載入到如 Redis 等快取中,通過快取提供資料訪問服務。這樣既可以降低資料庫的壓力,還可以提高資料的訪問效能。
另外,對需要模糊查詢的高頻資料,你也可以選用 ElasticSearch 等搜尋引擎。
六、前後序業務資料的處理
在微服務設計時你會經常發現,某些資料需要關聯前序微服務的資料。比如:在保險業務中,投保微服務生成投保單後,保單會關聯前序投保單資料等。在電商業務中,貨物運輸單會關聯前序訂單資料。由於關聯的資料分散在業務的前序微服務中,你無法通過不同微服務的資料庫來給它們建立資料關聯。
如何解決這種前後序的實體關聯呢?
一般來說,前後序的資料都跟領域事件有關。你可以通過領域事件處理機制,按需將前序資料通過領域事件實體,傳輸並冗餘到當前的微服務資料庫中。
你可以將前序資料設計為實體或者值物件,並被當前實體引用。在設計時你需要關注以下內容:如果前序資料在當前微服務只可整體修改,並且不會對它做查詢和統計分析,你可以將它設計為值物件;當前序資料是多條,並且需要做查詢和統計分析,你可以將它設計為實體。
這樣,你可以在貨物運輸微服務,一次獲取前序訂單的清單資料和貨物運輸單資料,將所有資料一次反饋給前端應用,降低跨微服務的呼叫。如果前序資料被設計為實體,你還可以將前序資料作為查詢條件,在本地微服務完成多維度的綜合資料查詢。只有必要時才從前序微服務,獲取前序實體的明細資料。這樣,既可以保證資料的完整性,還可以降低微服務的依賴,減少跨微服務呼叫,提升系統效能。
七、資料中臺與企業級資料整合
分散式微服務架構雖然提升了應用彈性和高可用能力,但原來集中的資料會隨著微服務拆分而形成很多資料孤島,增加資料整合和企業級資料使用的難度。你可以通過資料中臺來實現資料融合,解決分散式架構下的資料應用和整合問題。
你可以分三步來建設資料中臺。
第一,按照統一資料標準,完成不同微服務和渠道業務資料的彙集和儲存,解決資料孤島和初級資料共享的問題。
第二,建立主題資料模型,按照不同主題和場景對資料進行加工處理,建立面向不同主題的資料檢視,比如客戶統一檢視、代理人檢視和渠道檢視等。
第三,建立業務需求驅動的資料體系,支援業務和商業模式創新。
資料中臺不僅限於分析場景,也適用於交易型場景。你可以建立在資料倉儲和資料平臺上,將資料平臺化之後提供給前臺業務使用,為交易場景提供支援。
八、BFF 與企業級業務編排和協同
企業級業務流程往往是多個微服務一起協作完成的,每個單一職責的微服務就像積木塊,它們只完成自己特定的功能。那如何組織這些微服務,完成企業級業務編排和協同呢?
你可以在微服務和前端應用之間,增加一層 BFF 微服務(Backend for Frontends)。BFF 主要職責是處理微服務之間的服務組合和編排,微服務內的應用服務也是處理服務的組合和編排,那這二者有什麼差異呢?
BFF 位於中臺微服務之上,主要職責是微服務之間的服務協調;應用服務主要處理微服務內的服務組合和編排。在設計時我們應儘可能地將可複用的服務能力往下層沉澱,在實現能力複用的同時,還可以避免跨中心的服務呼叫。
BFF 像齒輪一樣,來適配前端應用與微服務之間的步調。它通過 Façade 服務適配不同的前端,通過服務組合和編排,組織和協調微服務。BFF 微服務可根據需求和流程變化,與前端應用版本協同釋出,避免中臺微服務為適配前端需求的變化,而頻繁地修改和釋出版本,從而保證微服務核心領域邏輯的穩定。
九、分散式事務還是事件驅動機制?
分散式架構下,原來單體的內部呼叫,會變成分散式呼叫。如果一個操作涉及多個微服務的資料修改,就會產生資料一致性的問題。資料一致性有強一致性和最終一致性兩種,它們實現方案不一樣,實施代價也不一樣。
對於實時性要求高的強一致性業務場景,你可以採用分散式事務,但分散式事務有效能代價,在設計時我們需平衡考慮業務拆分、資料一致性、效能和實現的複雜度,儘量避免分散式事務的產生。
領域事件驅動的非同步方式是分散式架構常用的設計方法,它可以解決非實時場景的資料最終一致性問題。基於訊息中介軟體的領域事件釋出和訂閱,可以很好地解耦微服務。通過削峰填谷,可以減輕資料庫實時訪問壓力,提高業務吞吐量和處理能力。你還可以通過事件驅動實現讀寫分離,提高資料庫訪問效能。對最終一致性的場景,我建議你採用領域事件驅動的設計方法。
十、多中心多活的設計
分散式架構的高可用主要通過多活設計來實現,多中心多活是一個非常複雜的工程,下面我主要列出以下幾個關鍵的設計。
- 選擇合適的分散式資料庫。資料庫應該支援多資料中心部署,滿足資料多副本以及資料底層複製和同步技術要求,以及資料恢復的時效性要求。
- 單元化架構設計。將若干個應用組成的業務單元作為部署的基本單位,實現同城和異地多活部署,以及跨中心彈性擴容。各單元業務功能自包含,所有業務流程都可在本單元完成;任意單元的資料在多個資料中心有副本,不會因故障而造成資料丟失;任何單元故障不影響其它同類單元的正常執行。單元化設計時我們要儘量避免跨資料中心和單元的呼叫。
- 訪問路由。訪問路由包括接入層、應用層和資料層的路由,確保前端訪問能夠按照路由準確到達資料中心和業務單元,準確寫入或獲取業務資料所在的資料庫。
- 全域性配置資料管理。實現各資料中心全域性配置資料的統一管理,每個資料中心全域性配置資料實時同步,保證資料的一致性。