l

2020年9月10日 星期四

Clean Architecture之CQRS Pattern

September 10 14:20~15:20

▲跳出盒子思考,有時候跨層存取也是OK滴


緣起

前幾天Teddy在讀《Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#》,看到第3章有一個段落談Code Structure Within a Bounded Context。這是一個很有趣的議題但談得人並不多,既使是這本書也沒有講得很詳細,但其中有一張如圖1所示的圖引起我的注意。


▲圖1:每一個end-to-end功能橫切軟體架構的每一層,圖片節錄自《Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#》,53頁。

***

Clean Architecture

看到圖1讓Teddy想起《Clean Architecture》第34章談到四種區分package的方法,回頭把這一章又讀了一次,看到圖2這段文字:


▲圖2:《Clean Architecture》 311頁。


當年Teddy在讀這段文字的時候並沒參透這個問題:為什麼在CQRS架構中就特意允許跨層參考?」雖然Teddy知道CQRS架構模式但自己沒有實際實作過,但這次讀懂了。

前陣子讀了一本奇書:《CQRS》。如圖3所示,書中明白建議:

  • 領域驅動設計(Domain-Driven Design;DDD)只需要應用在那些會改變系統狀態的Command。換句話說,只需要在Command套用DDD與Clean Architecture。
  • 針對不會改變系統狀態的Query不需要套用DDD,甚至根本也不要設計領域物件,直接讀取資料庫然後產生前端需要的View Model即可,這樣速度更快。


▲圖3:節錄自《CQRS


看完這本書之後Teddy就把ezKanban的use case改成Command與Query。昨天進一步和ezKanban團隊把帳號管理專案的package改用package by feature的方式來管理,如圖4所示。

▲圖4:ezKanban的帳戶管理專案採用package by feature


在重構成package by feature的過程中,GetUserUseCase這個Query與其它會改變系統狀態的use case分別放到不同的package。此時GetUserUseCase已經完全沒有對於User這個領域物件的依賴,如圖5所示它直接透過新的UserQueryRepository查詢資料庫並回傳UserDto給前端。換句話說,CQRS的Query端,故意省略domain model與資料庫之間的轉換,跳過domain model直接讀取資料庫。也就回答了圖2的問題。


▲圖5:直接操作資料庫並回傳DTO給前端


***

友藏內心獨白:繞了一大圈啊。

4 則留言:

  1. 此時GetUserUseCase已經完全沒有對於User這個領域物件的依賴

    有點疑問這句話的意思, 這個意思是說
    重新定義一個
    跟 User 領域物件一樣的物件來回傳
    藉此避免與原本領域物件依賴嘛?
    .
    如果將來 user 領域物件有變動
    uerdto 是否會有忘記更改的情況?

    回覆刪除
    回覆
    1. 基本上Query端不需要跟領域物件耦合,因為CQRS最大的好處就是“切割Read和Write,針對各自特性進行優化”,Write(DDD): 封裝Business Invariant、Read(SQL): 加強讀取效能,而這樣可以達到Loose Coupling的優點(Read Write Decoupling),但是反之也會有一些程式碼重複問題(DRY),但是設計取捨之下,遵守Loose Coupling的好處大於遵守DRY的好處。 所以是有可能重複導致修改有兩個地方,但是大部份時候影響不大

      刪除
    2. 這是否就代表圖片中的"ConvertUserToDto"已經不再需要了呢?

      刪除
    3. 對,從UserQueryRepository直接回傳UserDto,RDB的query repository已經不需要ConvertUserToDto。

      但我還有另外一個給 event sourcing系統使用的UserQueryRepositoryEsDbImpl,這個repository直接透過domain events產生User物件,所以還是需要ConvertUserToDto把User物件轉成UserDto。

      刪除