September 17 16:20~17:57
▲被封裝在聚合內部的咪咪
工商服務
想了解本系列文章完整內容,請參考【重構既有系統:邁向整潔架構實作班】。課程介紹與報名網址在此:https://teddysoft.tw/courses/refactor-to-ca/。
***
前言
上一集<重構既有系統,邁向整潔架構 (4):第一回合,分層與移除基本型別依戀>已經形成了基本的領域模型,這一集要在領域模型中進一步套用領域驅動設計(DDD)的戰術模式(Tactical Design Pattern),找出聚合(Aggregate),達到封裝與決定交易邊界的目的。
***
領域模型: 決定Entity, Value Object與Aggregate
圖1是上一集重構後的領域模型,ToDoList, Project, Task是Entity,Project Name是Value Object。為了簡化起見,Teddy將整個領域模型包成一個ToDoList Aggregate。
在DDD中,Aggregate與Repository是一對一的關係,一個Aggregate透過一個Repository存入資料庫。決定了Aggregate的邊界,之後如果有儲存領域模型狀態的需求(絕大多數的軟體系統都會有持久化的需求),只需要實作ToDoListRepository即可。
▲圖1:領域模型的四個類別
***
將Entity Id升級成Value Object
DDD的Entity是一個有「唯一識別符號(unique ID)」的物件,原本的ToDoList缺少這個id,因此新增ToDoListId value object作為它的id。Project可以用ProjectName當作它的id,至於Task有一個long id屬性可以當作它的id,但是考慮到以後Task id有可能是使用者自己指定的字串,因此一併幫它新增TaskId value object作為它的id。
增加兩個Value Object之後的領域模型如圖2。
▲圖2:領域模型現在有六個類別
***
封裝聚合
DDD的Aggregate是一個交易邊界,同時也是一個封裝單位。操作Aggregate內部物件的動作必須透過AggregateRoot,以避免客戶端破壞Aggregate Invariant。如果Aggregate回傳它內部Entity給客戶端,客戶端不可以直接修改這個Entity,以避免客戶端繞過Aggregate Root修改了Aggregate。
換句話說,Aggregate如果回傳內部Entity參考給外部物件,則這個Entity應該是唯讀物件。
舉個例子,圖3是ToDoList(Aggregate Root)的getProjects()方法,回傳其內部的List<Project>。客戶端拿到這個List<Project>物件之後,有兩個途徑可能繞過ToDoList而破壞封裝:
- 直接在List中增加一筆Project。
- 直接修改某個Project的內容,例如在Project身上新增一個Task。
第一點可以透過回傳一個「不可修改的List」來避免,至於第2點就只能靠ToDoList將Project轉成自己設計的ReadOnlyProject來避免。
▲圖3:ToDoList::getProjects() 程式碼
ReadOnlyProject的實作很簡單,請參考圖4。它直接繼承Project,然後覆寫所有會改變狀態的methods,直接丟出UnsupportedOperationException。
▲圖4:ReadOnlyProject程式碼(部分)
Project與Task都需要一個唯讀版本,重構後的領域模型現在有8個類別,請參考圖5。
▲圖5:領域模型成長到8個類別
***
下集預告
經過一番努力,重構至此領域模型終於有物件導向領域模型的樣子。但是一開始看起來很「礙眼」的TaskList程式依然沒變,還是原本那個150行、看起來亂亂的樣子。沒關係,下一集Teddy再來對付它。
***
友藏內心獨白:重構也要價值驅動。
沒有留言:
張貼留言