l

2021年9月5日 星期日

領域驅動設計學習筆記(20):再談Aggregate Root實作

September 05 20:00~21:30


前言

Teddy之前曾用三集談過Aggregate的基本實作注意事項:

後來在今年學了DCI之後,又從DCI的觀點將Aggregate重構一波:

今天要談的問題,在〈領域驅動設計學習筆記(6):Aggregate (中)〉曾經討論過,就是Aggregate Root應該回傳唯讀的內部Entity給客戶端。原本Teddy採用reflection-util這個工具自動產生immutable proxy。reflection-util需要使用JDK 8以及之後的版本,後來Teddy改用JDK 16之後,發現reflection-util不支援Record類別。

Record類別本身就是不可修改,其實不需要幫它產生immutable proxy。但reflection-util以為Record是一般的類別,會試圖幫它產生一個Proxy(透過子類別的方式)。因為Record類別是final,無法產生子類別,所以reflection-util就破功了。

***

替代方案1: 修改 reflection-util原始碼

▲ImmutableProxy類別透過isImmutable方法判斷哪些型別本身就是不可修改的型別


看了reflection-util的原始碼,其中ImmutableProxy類別的isImmutable用來判斷一個物件是否為Immutable,如果是就不需要幫它產生一個Immutable Proxy。由於ImmutableProxy類別本身是final,而且isImmutable又是static,因此只能修改原始碼,把Record加入判斷中,如此一來就可以在Java 16中繼續使用reflection-util

解決了不支援Record的主要問題,又發現reflection-util不認得Teddy自己設計的final類別,如下圖所示:


reflection-util遇到不認識的final類別也會出錯,無法產生Immutable Proxy。

Teddy認為比較好的方式是ImmutableProxy可以接受從外部注入一個isImmutable方法,如此一來便可動態決定哪些類別原本就是Immutable,不需要再幫它們產生Immutable Proxy。

經過一番嘗試之後,雖然可以修改reflection-util,但Teddy不想自己維護一份原始碼,再加上套用Clean Architecture的情況下盡量不要在Entity Layer使用外部函式庫,所以就決定放棄這個方法。

***

替代方案2: 老師傅手工打造Immutable Proxy

reflection-util原始想法是可以幫任意物件產生一個Immutable Proxy,雖然很方便但因為功能很通用所以遇到的問題(挑戰)也比較多。Teddy本來想偷懶直接使用reflection-util,後來念頭一轉想說算了,自己動手幫Aggregate Root打造合適的Immutable Proxy。

先幫鄉民回憶一下ezKanban的領域模型,下圖是Workflow Aggregate類別圖,其中Workflow是Aggregate Root,其他類別是Entity。

▲Workflow aggregate類別圖


如下列所示,Workflow有幾個方法會回傳Lane或是List<Lane>:

List<Lane> getRootStages()
      Optional<Lane> getRootStage(LaneId laneId)
      Optional<Lane> getLaneById(LaneId laneId)

      理論上使用者不可以透過回傳的Lane或是List<Lane>來修改Workflow的狀態,否則便違反了在DDD中由Aggregate Root確保自身狀態正確性的規定。

      為了回傳不可修改的Lane,首先新增一個ImmutableLane,類別讓它實作Lane介面,如下圖所示。

      ▲代表不可修改的ImmutableLane類別

      眼尖的朋友可能看出來了,ImmutableLane套用Proxy設計模式,它包裝一個原始物件(real subject),並且攔截對原始物件的呼叫。在這裡,所有會改變Lane狀態的方法,例如removeLanes()與addCommittedCard(),ImmutableLane的實作直接丟出一個UnsupportedOperationException,透過這個runtime exception用來物件代表不支援此操作。

      接下來只要修改Workflow(Aggregate Root),讓它回傳內部Entity的時候,也就是回傳Lane或List<Lane>,傳回ImmutableLane即可,如下圖所示。

      ▲修改Workflow讓它回傳ImmutableLane

      ***

      最後寫幾個測試案例驗證Workflow回傳的Lane與List<Lane>真的是不可修改的Lane。

      ***

      後語

      「Aggregate Root回傳不可修改之內部Entity的較佳實作方法到底是什麼?」這件事在Teddy心中放了好一陣子,之前一度使用reflection-util但後來因為前述的原因就沒再使用,因此ezKanban現有的Aggregate Root並沒有嚴格遵守這條要求。

      這幾天Teddy之所以會再注意這件事,是因為前兩天ezKanban團隊與OIS團隊一起mobbing的時候,團隊在event handler裡面寫了一段程式碼,類似:

      workflow.getRootStagebyId(stageId).setWipLimit(WipLimit.UNLIMIT);

      原本設定WIP Limit會產生WipLimitSet領域事件,但是系統跑起來之後卻沒有收到這個領域事件。過了一會兒大家才想起來,啊,不可以直接操作Aggregate內部的Entity來改變Aggregate狀態,要透過Aggregate Root來改變系統狀態。所以程式要改成:

      workflow.setLaneWipLimit(stageId, WipLimit.UNLIMIT);

      ***

      人畢竟是人,如果你問Teddy:「客戶端是不是不能直接修改Aggregate內部的Entity來改變Aggregate狀態?」Teddy會不假思索的回答:「是。」但寫程式的時候,有時一恍神就會寫出下面這種錯誤的程式碼:

      workflow.getRootStagebyId(stageId).setWipLimit(WipLimit.UNLIMIT);

      所以,做人還是不要偷懶,乖乖地讓Aggregate Root回傳不可修改的內部Entity就可以避免這種程式錯誤。

      ***

      友藏內心獨白:犯錯是人的天性,能避免的錯誤還是交由系統來處理。就好像「自主健康管理」有用就不需要集中檢疫了 XD。

      沒有留言:

      張貼留言