l

2024年9月17日 星期二

重構既有系統,邁向整潔架構 (5):第二回合,套用DDD戰術模式

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而破壞封裝:

  1. 直接在List中增加一筆Project。
  2. 直接修改某個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再來對付它。

***

友藏內心獨白:重構也要價值驅動。

沒有留言:

張貼留言