無法實施富領域模型的罪魁禍首找到了
本文轉載自微信公眾號「codeasy」,作者閻華 。轉載本文請聯系codeasy公眾號。
要弄清楚使用富領域模型有什么問題,我們要先從應用服務層怎么使用富領域模型說起。
應用服務是編舞者
領域模型具有了行為以后,就成為了一個個動作靈活的舞蹈家,但很多情況下觀眾并不想只看某一個舞蹈家展示他們的動作,所以,應用服務需要把一個或若干個領域模型的行為編排起來,來完成符合某個場景(Use Case)需要的一支舞蹈。
我們來看看 SprintApplicationService 這個應用服務里的一個方法:
- /**
- * 將一個BacklogItem提交到一個Sprint中
- * @param aCommand 表示客戶端發起的一個命令
- */
- public void commitBacklogItemToSprint(
- CommitBacklogItemToSprintCommand aCommand) {
- TenantId tenantId = new TenantId(aCommand.getTenantId());
- //Step1:加載一個sprint到內存
- Sprint sprint =
- this.sprintRepository()
- .sprintOfId(
- tenantId,
- new SprintId(aCommand.getSprintId()));
- //Step2: 加載一個BacklogItem到內存
- BacklogItem backlogItem =
- this.backlogItemRepository()
- .backlogItemOfId(
- tenantId,
- new BacklogItemId(aCommand.getBacklogItemId()));
- //Step3:將BacklogItem提交到一個sprint,內存級操作
- sprint.commit(backlogItem);
- //Step4:持久化sprint
- this.sprintRepository().save(sprint);
- }
這里Sprint和BacklogItem是兩個聚合根,他們分別對應了一組實體。
第一步和第二步從數據庫加載了兩個聚合到內存,在內存里有兩個對象圖:
而當我們執行完第三步時(即執行 sprint.commit(backlogItem) 后),內存里的對象圖變成了:
這時,在 sprint的backlogItems 這個集合里,多出一個 cb3 ,它 的 ordering 是 3 , backlogItemId 是 12 。
當把 id 是 12 的 backlogItem 加入到 spint 里,需要做一些校驗,以及新產生一個 cb3 并正確設定它的 ordering 的值,這些都是 sprint 這個聚合內部發生的邏輯,應用服務是不知道這些領域邏輯的,甚至都不知道有這些邏輯的存在。
更復雜的場景,可能導致聚合內多個對象的內存狀態發生了變化。
注意,這時候只是內存里對象的狀態發生了變化。到了第四步時,應用服務委托 sprintRepository 去持久化 sprint 后,內存對象的變化才會反應到對應的數據庫的表(一個或多個)內容的變化(即更新或插入了數據)—— 導致多少表的什么變化,應用服務也是不知道的。
正是由于這樣職責劃分,才會出現我們第一篇文章里看到的結果 —— 領域層的代碼很豐富,而應用層的代碼很少,只有這里看到的編排邏輯。這帶來的好處前兩篇文章說了很多了,不再贅述了。
但這樣做有什么問題呢?
為什么會投鼠忌器
我們說不敢使用富領域模型一定是有顧慮的,既然投鼠忌器,那這個“器”是什么呢?
可能有些人已經看出來了,“器”有兩個:
- 性能
- 并發沖突
如果我們只是按面向對象的設計方法去實現富領域模型,可能會導致對象關聯太多,內存中的對象圖會是下面這個樣子:
image.png
連線表示對象引用
那在應用服務層可能會加載非常多的對象到內存里,很費內存。另外,修改時可能導致很多對象狀態的變更,修改引發的并發沖突會比較多。
DDD恰恰是要解決這個問題的,它推薦把對象分成不同的“小組”,也就是我們前面說的聚合。聚合和聚合之間是不能做對象引用的,只能用ID引用,這樣加載一個聚合時不會把其他聚合也加載到內存。
image.png
黑色的鏈接線表示的是ID引用
總結一句話,要通過小的聚合來避免性能和修改的并發沖突問題。
但是……
聚合要多小才合適
但是聚合多小才算合適呢?極端情況下,一個表一個聚合就足夠小了,但這又回到了貧血模型。
聚合還是要代表一個業務一致性邊界的,比如OrderItem的屬性變化,和Order的屬性變化應該保證一定的業務規則不被破壞,在這個前提下,聚合要設計的盡可能小。
從IDDD_Sample的代碼里,我們是看不到設計聚合的分析過程的,只能看到結果,想知道分析的過程,推薦去看《實現領域驅動設計》書中對這個例子的分析過程。
我們在后續的文章會分析另外一個開源示例(Library),那個例子里給出了分析過程的記錄,到時候再詳細講解聚合的識別過程。
在我實踐的過程中,發現大部分人設計的聚合都偏大。最近我嘗試使用領域故事會和事件風暴這兩個方法來識別聚合,發現得到的聚合比以前的更小更合理。這得益于基于場景去分析,而不是從技術的角度去建模。
《領域驅動設計模式、原理與實踐》是另一本非常棒的關于DDD的書,里面曾經說過“如果你發現一個聚合可能會帶來性能和并發的問題,就要回過頭去看看聚合是不是設計的太大了”。之前我一直覺得這是因果倒置的無奈之舉。現在感覺是有合理的邏輯在的:
- 按場景分析的話,聚合的粒度會比較小
- 如果發現有性能和并發的問題,說明聚合太大了
- 那可能是沒有按場景分析,所以要再按場景重新審視一下聚合的設計
所以這個技術問題本質上是一個模型分析/設計的問題,但分析/設計的問題比技術問題更難解決,更難有固定的套路。后續我也打算寫另外一個系列,是關于DDD設計過程中的反模式的,其中就有很多是關于不合理的聚合設計的。
接下來聊聊CQRS
現在,我們還是聚焦在IDDD_Sample示例的代碼分析。聚合設計過大其中有一個原因,是開發人員考慮了太多的查詢的需要。合理地使用CQRS模式可以避免這個問題。另外,使用CQRS本身也能解決很多的性能問題。
我們下一篇看看IDDD_Sample中是怎么運用CQRS這個模式的。