6

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

 2 years ago
source link: https://teddy-chen-tw.blogspot.com/2021/09/20aggregate-root.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

September 05 20:00~21:30

AM-JKLWb9hXQBjP3w_KQqW_g4GSQ9Ck0ebVGt3e11Q-kTzWnhsutP2CbD91oVeLLFCHJpMsIXQn5n__MsA1GnzQIUGogF4Q5yJDBdmaoeuTn9cAZXIK_VK-9j6nSMsktRKrTKOYeinx5OMheFFX6V05cHDMmwQ=w1352-h478-no?authuser=0

前言

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原始碼

AM-JKLWnGfwQb75Qs8EanuBdRIp9HkvbiT84UoXbwRxTb5YlZhgtmQDjm02E19ZI15gEpJ23RRPr3bauV0gVsm_3rXu_403VxC10ihmBprP4PandjdKvgQPYeVglifM9-of46_vaKzqvcc6sjVipZEHCA3idgA=w1012-h1490-no?authuser=0

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

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

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

AM-JKLVN2-QoVQWXLSoHUZ2oetYJ-4xaaTqGNyOoBVQggFUbtEgqsuvDk5CUNN94Vn71gSVK4to_B2nFO5W7P0IcPpyLyliwFJRuTYQRdjS6ANUUEP47VKk2MIXmPVcEIqQ8chKXvIItSVOxEPUpO6d2w2JdHQ=w1122-h288-no?authuser=0

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。

AM-JKLVgpFZO1WvowbRD3YEIsjBSLjyoYfdJ7VO5ah1_7d-apQTuxACzTI5dt7m0w6-BnCRZhp9V3sc4mIKeNXg-dKI2-ncyhTUdwE-IxjIxD0N9trQnJ6BOCHhEE4zLrGDKZj_cTIwnWwKFyXB8pH7HGgGxcA=w1924-h1116-no?authuser=0

▲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介面,如下圖所示。

AM-JKLVFG91ri8Q9-P-z1BhtVOH9gDPVNyPnHzSw2qvzn-ZBit_iYVRm48A0nQvaY9b4KsU0K7VwZvt15lkKdO9Z3X7_ww7lte_cSBk0IZLF6zJU0OL4vSCWwu_RcOEpLIgRxyloLx2foyPj419oZk64umL0VQ=w1588-h1522-no?authuser=0

▲代表不可修改的ImmutableLane類別

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

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

AM-JKLUA5LMBQDMhpxbLTnaurUxf1Xr6QXlcXYYBg7ZdLvaY_IgYV8d23cIZtN2Mfny91TfhrSg3JUQSs9I3OjXYdHhwHF7lLaQSZ1DH9SHMaef7iqHN_D5RYmlArLaIPoOFNonqTMf7AxY1cHH3pY60zQi8lw=w2952-h1092-no?authuser=0

▲修改Workflow讓它回傳ImmutableLane

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

AM-JKLW0WCs53BwYgTz7nM1lGiYy4debpTUNTB1iiPj5B4ss4HHsHAUm9gu7Cc_GbMx0qYgWq5L7B7wG8V_MEovxpLKYaMfzQFce1O5m3fCwlCmEY5B8NwBqiGpVDM2kgZlr7ePyCxAIrzBjqhWO4Mry_1-WBQ=w2946-h798-no?authuser=0
AM-JKLXWZm4tLGXK4Z6c5G9N1EfOJXJ3PfpTbihWmDSajkICpdmcMU8LLoqqP5WWUWD2i4H13fByOy6zAzd4DdTXsIWPbYSbYTey4k5_UqS3c0qf42U8WLJsqmwJ7bj-04veUD2oscuzsPfhf31Y-dwmMvTiUg=w1352-h478-no?authuser=0

後語

「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。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK