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給前端


***

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

2020年9月8日 星期二

再談以前端為主的思考對於領域模型的影響

September 08 11:25~13:30

▲大部分的人都是「外貌協會」成員,UI當然非常重要,但不適合從UI來驅動領域模型的設計。

***

工商服務

想要看見別人看不到的設計盲點,想要知道怎麼套用設計模式才是恰如其分的設計,想要跟同事針對設計問題吵架時可以嘴砲勝利,歡迎參加第22梯次【Design Patterns這樣學就會了–入門實作班】,招生中。

***

背景介紹

前幾天Teddy談了〈為什麼不要把後端的領域物件直接傳給前端?〉,今天再舉一個同樣是在開發ezKanban所遇到的例子,討論以前端UI為主的思考方式,會如何影響到後端的領域模型,造成不必要的耦合。

約兩周前Teddy和ezKanban團隊開發Tag的功能,讓使用者可以幫看板中的卡片用Tag來分類,如圖1。


▲圖1:卡片上的多個Tags


ezKanban是一個支援多人同時使用的即時協作系統,如果很多人同時操作同一個看板,系統必須自動保持前端狀態同步。如圖3所示,ezKanban使用WebSocket做為後端主動通知的管道。當前端使用者執行某一個功能,會呼叫後端的Use Case。例如,呼叫CreateTagUseCase新增一個Tag。Use Case執行完畢後產生TagCreated Domain Event(領域事件),後端會將這個TagCreated Domain Event(轉成DTO)廣播給在同一個Board的使用者。當前端收到廣播的Domain Event之後,只要依據Domain Event的種類與內容,在前端直接更新本地UI狀態,便可達到狀態同步的目的。


▲圖3:ezKanban透過WebSocket廣播需要即時同步的領域事件

***

前端需要顯示Tag Name

當前端收到後端傳來的Domain Event,會把它顯示在畫面右上角,讓使用者知道系統狀態已經改變。參考圖4,Teddy把feature這個Tag指定給某張卡片,因此前端顯示如下訊息:「Teddy assigned a tag feature to card」。


▲圖4:前端將收到的領域事件顯示在畫面上


要顯示領域事件就顯示啊,有什麼好說的?」問題就出在這顯示的內容上面。ezKanban是採用領域驅動設計(Domain-Driven Design;DDD)+ Event Storming + Clean Architecture + TDD的方式開發,請參考圖5,原本的Event Storming關於Assign Tag的輸入,並沒有tag name,因為把一個Tag貼到Card上面並不需要tag name,只要有tag id即可。但是如果沒有tag name,要發TagAssigned Domain Event的時候會少了這個資料,導致前端無法顯示tag name。


▲圖5:前端將收到的領域事


因此當下ezKanban團隊的第一個反應就是:「把tag name加到AssignTagUseCase的輸入參數,再把它傳給Card,這樣子Card在發Domain Event的時候可以有tag name。

這就是因為前端顯示需求而影響後端use case layer與entity layer的例子。

***

啊不然勒?

「Separation of Concerns」,我們不應該只是因為前端顯示的需求而污染了use case layer與entity layer,特別是entity layer要好好保護它。既然都有了tag id,其實只要在將Domain Event傳給前端的時候,依據tag id 去找出tag name,然後把tag name塞到傳給前端的Domain Event DTO就可以了。最後實作的程式碼如圖6所示。


▲圖6:將領域事件傳給前端前在把tag name塞進去即可

***


不是不重要

這幾天討論前端與後端領域模型的這兩篇文章,以及之前一系列Clean Architecture/DDD談到資料庫是細節,不應該從資料庫來思考軟體設計,並不是說前端、UI、資料庫不重要。它們都很重要,軟體是一個整體,每一層都做好了,才能夠交付end-to-end價值。

但是,還是那句老話:Separation of Concerns。在討論複雜的業務邏輯的時候,就應該以問題領域的業務邏輯為主去思考,而不是從使用者介面或是資料庫表格的設計,來反推業務邏輯與領域物件應該怎麼設計。

把主要的事情確定了,其他次要的事自然會浮現。

***

友藏內心獨白:先後順序不要搞錯,要先脫衣服再洗澡,不要洗完澡才脫衣服。


2020年9月7日 星期一

PoEEA之Server Layer

September 07 22:36~23:20

▲PoEAA書中提到四種表達領域邏輯的模式


緣起

好一陣子沒幫部落格文章增加新分類,今天這一篇開啟【盡信書不如無書】這個分類,紀錄Teddy讀書時看到一些自己覺得怪怪的地方。

***

哪裡怪
前幾天Teddy在準備Asian PLoP 2020的演講題目:〈Pattern-Based Problem Solving: One Pattern at a Time〉,翻到《Patterns of Enterprise Application Architecture》第九章Domain Logic Patterns,看到Service Layer這個模式,如圖1所示。


▲Service Layer模式,節錄自《Patterns of Enterprise Application Architecture (PoEAA)》,133頁。


這本書2003年出版至今已有17年,當年Teddy讀到這個模式、看到這張圖,並沒有什麼感覺。但因為這幾年學了Clean Architecture,前幾天再次看到這張圖,當下就覺得不對勁:「Data Source Layer怎麼會畫在軟體架構的最核心?」。

***

資料庫是細節

Clean Architecture四層架構如圖2,資料庫屬於最外層,並非如PoEAA所畫的位於最核心。軟體架構的核心應該是Entity Layer,也就是Domain Model Layer。


▲圖2:《Clean Architecture》建議的四層架構,畫成同心圓(圖片來源在此


在階層式架構中,把資料庫畫成底層或是核心層,在N年前也算是常見的畫法。但以現在的角度來看,對照圖1與圖2,應該可以很清楚看出來,圖2的觀點比較正確。資料庫、使用者介面、框架、驅動程式等,都屬於細節,屬於階層式架構的外層,並非是核心部分。

其次,傳統階層式架構所說的Service Layer,就是Clean Architecture裡面的Use Case Layer。Teddy現在覺得Use Case Layer比較具體,因為Service這個字有很多種含意,因此用Service Layer來代表應用程式所提供功能或服務的邊界,好像有點那麼不是很直覺(就跟這句話一樣XD)。

***

讀書不是照單全收

PoEAA是一本很棒的書,書中所整理的模式很多到今日依然適用且日久彌新。但也有一些模式的內容,因為時代演進需要稍微修正,像今天介紹的Service Layer就是一個例子。

讀書不是照單全收,有時候同意,有時候疑惑,有時候反對。理論上,除非這本書有問題,否則同意的時候應該居多。疑惑時可能是自己離作者太遠,尚無法理解書中的微言大義,但也能不排除內容有誤的能性。

反對,代表自己有看法,這個看法與書中不同。自己的看法可能是錯的,也可能是對的。能夠說出不同,也算是一種讀書的層次。

***

友藏內心獨白:挑毛病也要講出個道理。

2020年9月6日 星期日

為什麼不要把後端的領域物件直接傳給前端?

September 06 20:50~22:22

▲ezKanban的tag功能


前幾天有一位朋友問Teddy:「Clean Architecture提到不要把Entity Layer的物件直接傳到UI端,但真的有需要跨層的時候把物件都轉成DTO再往外傳嗎?」

***

以ezKanban為例子

ezKanban是一個支援多人同時使用的看板系統,圖1為其簡化版的領域模型。Board代表一個看板,其中包含了若干個Workflow(工作流),每個Workflow可以有若干的Lane。Lane區分成兩種,垂直的Lane稱為Stage(階段),水平的Lane稱為Swimlane(泳道)。Card(卡片)可以被放到Lane上面,並在不同的Lane之間移動。


▲圖1:ezKanban簡化版領域模型


假設Board被直接傳到前端,前端設計出如圖2所示的畫面。

▲圖2:ezKanban畫面


到目前為止沒什麼問題,前端UI物件模型與後端的領域模型大致上是一致的。一直到有一天客戶有新的需求:

  • 希望增加Tag的功能來將Card分類
  • 每個Board都可以有自己的Tag,不會與其他Board共用Tag
  • 一張Card可以有多個Tag

如果從UI的角度來思考:「Board包含Tag,然後這些Tag才可以被指定到屬於這個Board的卡片裡面。」為了能夠在畫面上顯示Tag,前端的人很可能會要求後端將Tag加入Board物件成為它的屬性,變成如圖3所示的關係:


▲圖3:因為UI的需求,導致Board與Tag產生關係

***

問題出在哪裡?

直接把Entity Layer的物件傳給前端違反單一責任原則。Entity Layer的物件之所以會存在,是為了解決或是表達問題領域的重要概念與商業邏輯。如果把它們直接傳給UI,拿來當作顯示畫面之用,Entity Layer的物件就擔負了兩種責任:

  • 表達商業邏輯與概念
  • 表達顯示邏輯

如此一來Entity Layer的物件就可能因為兩種不同客戶端的需求改變而跟著改變,換句話說Entity Layer與UI產生耦合,導致程式難以理解、修改與維護。

***

如何解決?

回歸到Clean Architecture與領域驅動設計(Domain-Driven Design;DDD)的角度,先確定商業邏輯,至於使用者介面是屬於細節,等Entity Layer與Use Case Layer確定之後,再交給Presenter產生View Model來滿足前端顯示的需求即可

回到幫Card貼Tag的需求,把Tag歸類為Board的屬性並不合理,因為Tag並不是要貼在Board上面,使用者只需要知道這個Board裡面建了多少種類的Tag,再拿這些Tag貼在卡片上面。也就是說,Tag只需要知道它自己屬於哪一個Board的單向關係即可,Board根本不需要知道Tag,如圖4所示。


▲圖4:Board, Card與Tag的關係


至於前端所需的所有資料,如圖5所示另外設計一個BoardContentViewModel。這個View Model是由Presenter為了前端所需而動態產生,Entity Layer並沒有一個這樣的靜態Model。

▲圖5:ezKanban管理Tag畫面


最後的領域模型,Tag物件不屬於Board,使用者可以獨立新增與修改Tag,也很容易直接將Tag指定給Card,請參考圖6與圖7。


▲圖6:ezKanban管理Tag畫面


▲圖7:ezKanban顯示Card上面多個Tag

***

友藏內心獨白:UI歸UI,Domain Model要分明。