July 02 21:45~22:48
在上一集〈領域驅動設計學習筆記(5):Aggregate (上)〉提到為了避免破壞aggregate invariant(聚合不變量或聚合規則),aggregate root在回傳資料的時候,需要考慮:
- 完全禁止回傳Aggregate內部的Entity。
- 可以回傳Entity,但只回傳immutable Entity或是Entity的deep copy。
- 設計immutable interface,讓Entity實作immutable interface並透過immutable interface回傳給客戶端。
今天討論這幾種做法的優缺點,並展示一個透過自動產生immutable proxy的工具來解決這問題的範例。
***
禁止回傳Aggregate內部的Entity
- 優點:這個方法因為禁止aggregate root回傳內部entity,所以客戶端不可能透過aggregate內部entity來破壞aggregate invariant。
- 缺點:為了提供資料給客戶端,aggregate root可能需要將內部entity轉成DTO(data transfer object)再往外傳。這種做法有點類似clean architecture中,use case層定義雙向介面將entity往外傳遞。從架構的角度來看可以切得很乾淨,但是需要花費額外的功夫包一層DTO。
***
回傳immutable entity或是entity的deep copy
- 優點:用相同的domain model(直接回傳aggregate內部的entity)來處理程式邏輯,而且因為回傳的entity是不可修改的物件,或是原本物件的複製版本,所以也不可能透過這個entity破壞aggregate invariant。
- 缺點:無論是實作immutable entity或是deep copy(GoF的Prototype設計模式),都需要花費額外的設計、實作與測試的功夫。特別是deep copy,不小心沒有實作好變成shallow copy就GG了。
***
設計immutable interface
Immutable interface是一種設計模式,請參考c2。簡單講,immutable interface是一個只有getter沒有setter的介面。設計好immutable interface之後,讓你的entity實作此介面。如果aggregate root要回傳entity,只能傳回entity所實作的immutable interface,如此一來客戶端就不可能修改到aggregate的資料。
Immutable interface的缺點和回傳DTO類似,domain model裡面除了原本的entity,又多了另一種代表「不可修改」的immutable interface。
***
折衷作法
如果程式語言可以支援自動回傳immutable object,這個問題就不是問題。Teddy不知道有沒有哪個程式語言有這種功能,目前只能從上述三種做法中挑一種來實做。
Teddy想用方法二。方法二有兩種實作,考慮到回傳entity的deep copy比較不好實作,所以決定優先選擇回傳immutable entity的方式。
要實作immutable entity,可以套用Proxy設計模式,概念如下:
Ministage是一個mutable介面,MinistageImpl是這個介面的實作,而ImmutableMinistage則是將MinistageImpl包裝一層,只要客戶端呼叫到setter的函數,就丟出例外。
這種方法的優點是,從客戶端的角度,他看到的就是Ministage這個domain model的概念。至於它是mutable還是immutable,則只是實作細節。
這種做法當然也有缺點,首先設計上當然比沒套用Proxy設計模式要來的複雜。其次,因為在編譯期間(compile time)客戶端不知道拿到的Ministage是mutable還是immutable,如果不小心拿到immutable但卻呼叫到它的setter函數,則會出現runtime exception。
程式設計師都有偷懶的天性,Teddy想回傳immutable entity但又懶得手動實作proxy,於是google了一下,找到一個Java語言的reflection-util工具,可以自動產生immutable proxy。
▼使用方法很簡單,以maven建構工具為例,首先加入reflection-util的參考。
▼接著在程式中透過ImmutableProxy.create()方法,可以直接產生immutable entity,非常容易使用。
***
從Clean Architecture的角度來看…
使用reflection-util這種工具,可以幾乎無痛產生immutable entity。但是從clean architecture的角度來看,在最核心的entity層引用外部工具,違反了相依性原則,你的架構就變得不再那麼乾淨了。
Teddy覺得這個「小違規」目前是可以接受的折衷方案。因為你可以透過將reflection-util再包一層,讓aggregate root透過間接的方式呼叫reflection-util來獲得immutable entity。有朝一日如果真的不想使用reflection-util,也可以在影響最小的情況下,以其他實作方式將它取而代之。
所以,雖不完美,但還可接受。
***
友藏內心獨白:設計就是取捨後的結果。
我反而喜歡 immutable interface XD
回覆刪除https://medium.com/%E9%96%92%E8%AB%87%E8%BB%9F%E9%AB%94%E6%9E%B6%E6%A7%8B/%E9%96%92%E8%AB%87%E8%BB%9F%E9%AB%94%E6%9E%B6%E6%A7%8B-immutable-interface-bd59d6a77546
我覺得這個問題可能要從 domain model 來思考,在 domain expert 的口中,會不會有 UserInfo 或 MessageInfo 這種概念?還是只有 User 與 Message?Immutability 到底是一個 problem domain 的概念,還是實作細節,或是兩者都有?這個問題我要再想想...XD
刪除感覺這種實作細節一方面有種Critical Section的意味,另一方面也與某種演算法等效,存粹只是為了加速開發的手段吧?
刪除