l

2023年6月12日 星期一

該不該使用Unit of Work和Repository?

June 12 15:48~16:20;20:22~22:56


▲圖1:ezKanban的Repository介面 


前言

昨天提到6/5~6/7到客戶家上【領域驅動設計與簡潔架構入門實作班】,有一位學員問Teddy的三個問題:

  1. 為什麼Teddy建議Entities Layer的物件不要直接操作Repository?
  2. 為什麼有人說UoW(Unit of Work)和Repository在DDD裡面算是Anti-Pattern?
  3. 為什麼Teddy沒有用Specification模式?

昨天談了第一個問題,今天聊聊第二個問題:「在DDD中,UoW和Repository要不要使用」?

 

***

Unit of Work(UoW)

Unit of Work和Repository這兩個設計模式都出自於Martin Fowler所寫的《Patterns of Enterprise Application Architecture》。Unit of Work顧名思義就是工作單元什麼叫做工作單元?就是把一連串的工作步驟,視為「一個單元」、「一整包完整的大步驟」。一個工作單元隱含一個交易邊界(transaction boundary)

舉個例子,在ezKanban裡面,有以下幾個使用案例:

  • CreateBoardUseCase
  • CreateWorkflowUseCase
  • CreateStageUseCase
  • CreateCardUseCase

每一個使用案例,都是一個工作單元,形成一個交易邊界。產生一個Board是一個工作單元,要嘛成功,要嘛失敗。同理,產生Workflow、產生Stage、產生Card,都是一個工作單元。很簡單,對不對!

ezKanban團隊使用ezKanban的領域模型開發了看板桌遊,這是另一個Bounded Context。在看板桌遊中,有一個CreateKanbanGameUseCase用來產生新的看板遊戲。這個使用案例的實作,呼叫上述四個使用案例。現在問題來了:「在看板桌遊的Bounded Context中,CreateKanbanGameUseCase是一個工作單元,它的執行要嘛成功要嘛失敗。但是因為CreateKanbanGameUseCase的實作方式是重複使用CreateBoardUseCase、CreateWorkflowUseCase、CreateStageUseCase與CreateCardUseCase,這四個使用案例各自是一個工作單元,要怎麼把它們用另一個更大的工作單元包起來?」

Unit of Work設計模式就是要解決這個問題,簡單講,就是把transaction manager注入給使用案例,而不是讓使用案例自己去控制。如此一來,最外層的使用案例負責控制transaction的開始與結束,內部的使用案例只是接受這個由最外層使用案例所注入的transaction manager。如此一來,便可以因應不同使用情境(Context)的需要,動態決定工作單元的範圍。

***

 

為什麼不要使用Unit of Work?

如果不管DDD,Unit of Work是一個很棒的設計模式。但是,在DDD中,Aggregate已經形成了一個交易邊界。如果在DDD中需要使用Unit of Work,則代表在某個Context底下,需要把好幾個不同的Aggregate放在同一個交易中。這不就和原本在DDD中「Aggregate形成了交易邊界」互相衝突了。

更進一步來看,在DDD中,Aggregate由Repository負責儲存與讀取。而「理論上」一個Repository可以各自採用不同的資料庫來儲存Aggregate。也就是說,如果你願意,可以將Aggregate當成一個微服務來佈署。如果在DDD中使用Unit of Work,則這些被放在同一個Unit of Work的Aggregate,就代表它們要綁在同一個資料庫中(除非使用distributed transaction,但採用這種做法的人很少,因為會造成效能問題),這就造成不同的Aggregate透過資料庫產生耦合。因此,Teddy覺得在DDD中,不應該使用Unit of Work。

***

Repository可以用嗎?

在Martin Fowler的《Patterns of Enterprise Application Architecture》書中,Repository代表Collection-Based的儲存體。也就是說,只要從Repository拿出物件,之後對於該物件的修改,會直接反應回Repository,使用者不需要呼叫save方法來儲存該物件。

在Vaughn Vernon所寫的《Implementing Domain-Driven Design》,進一步將Repository的實作分成Collection-Based Repository與 Curd-Based Repository。圖1為ezKanban所設計的Repository介面,採用Crud-Based Repository。ezKanban的所有Aggregate所對應的Repository都是採用相同的介面,只有findById, save與delete這三個方法。

不管是Collection-Based或是Crud-Based,Teddy主張,只要固定Repository介面,將其限制在單一Aggregate的新增、修改、刪除、查詢,這樣子使用Repository並不會有什麼太大的問題。

但是,實務上經常可以看到,很多開發人員在Repository身上加了很多查詢方法。如此一來,雖著需求演進,Repository的介面越來越肥大。你可以說,這種使用Repository的方式,違反了單一責任原則、開放封閉原則,以及介面隔離原則。

所以,只要固定Repository介面,將其餘查詢方法另外設計(在ezKanban中採用Inquiry設計模式來解決這個問題),在DDD中使用Repository是沒有問題的。

***

結論

以上,是Teddy近幾年開發ezKanban所累積的經驗。Unit of Work比較簡單,ezKanban壓根就沒使用過它。但是,針對Repository的使用方法ezKanban團隊重構了好幾次。一開始Teddy也是在不同的Concreate Repository中直接新增個別Aggregate所需要的查詢介面。但隨著系統越來越複雜,Repository也變得越來越亂,不容易理解其中的邏輯。後來,套用CQRS之後,把查詢、命令分離,保留最簡單的Repository介面。如此一來,使用Repository就沒有問題了。

***

友藏內心獨白:不是不好用,是你不會用 XD。

沒有留言:

張貼留言