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要分明。

2020年8月21日 星期五

三種團隊互動模式

August 21 18:15~19:01


前言

Teddy跟著ezKanban團隊兩年的時間,帶著幾位北科資工研究生開發ezKanban系統。當初是以DDD與Clean Architecture為研究主題當作切入點,這兩年來雖然有點進度,但軟體本身卻還沒達到Teddy認為可釋出的標準。

這個兩年的lead time真的有點長,今年七月初放暑假時,Teddy決定利用暑假時間和ezKanban團隊採用remote mobbing的方式一起開發。學生在學校實驗室,Teddy在家裡,透過Skype分享桌面的方式開發ezKanban,一個多月下來頗有進展。

***

幾個禮拜前在Amazon亂買書,買了一本《Team Topologies: Organizing Business and Technology Teams for Fast Flow》,有一天晚上睡不著翻了一下,沒想到還挺有趣的。書中提到四種團隊型態以及三種互動模式,特別適合軟體開發公司。今天用Teddy這兩年來和ezKanban團隊的合作模式,解釋書中提到的三種互動模式。

***

X-as-a-Service

將另一個團隊視為一種服務來使用,團隊之間只有最少的協作。這種情況發生在學生遇到問題詢問Teddy的時候,此時Teddy-as-a-Service,提供學生某種解答,這個過程中雙方並沒有很多協作關係。

***

Facilitating

協助另一個團隊釐清阻礙,這種情況發生在兩周一次的sprint review。Teddy會看學生的設計與程式碼,觀察有沒有什麼設計問題是學生沒有看出來的,並與他們討論並訂定後續修正計畫。下個sprint review會繼續相同步驟,經過迭代的過層,系統設計品質越來越好。

但是,因為兩周review一次兩小時,說真的能夠看到的程式碼真的不多,因此每個迭代的增量幅度相對來講就比較有限。另外一個問題是,因為學生還在學習的過程,很多Teddy沒有看到的地方,雖然程式可以動,但設計通常都存在一些大大小小的問題,長期而言阻礙系統的可維護性。

***

Collaboration

兩個團隊彼此緊密合作。在今年暑假之前,Teddy和ezKanban團隊的關係幾乎不存在Collaboration(協作)。之前也幾度思考過是不是跟學生一起寫程式,但又擔心如此一來學生偷懶,過於依賴Teddy,搞到後來變成Teddy幫他們做碩士論文研究 Orz,因此就作罷。

但隨著ezKanban慢慢成形,但又無法達到Teddy心中的釋出要求,有種恨鐵不成鋼的遺憾。幾經思考,加上今年因為因為疫情的緣故,泰迪軟體生意比較清淡,Teddy空閒時間也比較多,因此決定「撩落去」,利用暑假時間一起合作開發。

***

成本不同

這三種互動模式,適合解決的問題不同,成本也不同。對Teddy而言,開發軟體如果可以和團隊緊密合作當然是效果最好的。畢竟Teddy也比學生多活了20幾年,還是比他們更能夠看出設計不合適之處。

除了學生獲益,Teddy也從這個過程中獲得很多design bad smells的寶貴範例,平常可能想破頭都不一定生的出來這些例子。這是一個雙方都互相學習的過程。

很多時候,想要成事,還是需要走到前線,親自動手。

***

友藏內心獨白:If you stop coding, you stop learning, by Kent Beck.

2020年8月20日 星期四

Clean Architecture(7):套用CQRS簡化Presenter與View Model

August 20 20:50~22:45


背景說明

《Clean Architecture》書中有一張圖解釋Clean Architecture在網頁程式的實作,如圖1所示。

▲圖1:典型的Clean Architecture實作

Use Case Interactor定義了Input Boundary、Output Boundary以及Input Data和Output Data,這些類別與介面都位於Use Case階層。

其中Output Boundary介面由Presenter實作。Presenter位於Interface Adapters,負責產生View所需要的資料,稱為View Model。

以上分層與類別/介面的關係看起來很簡單,在撰寫程式碼時,一個Use Case對應到一組Input與Output,Input由Use Case實作,Output由Presenter實作。理論上,一個Use Case會對應到一個Presenter。

***

太多Presenter

如果完全依照圖1的方式去開發,有多少個Use Case就會有多少個Presenter與View Model。圖2是ezKanban系統關於操作Board(看板)所設計出來的多個Presenter與View Model其中,其中包含:

  • addermember:加入組員
  • changetitle:修改標題
  • create:新增看板
  • delete:刪除看板
  • enter:進入看板
  • get:取得看板id與名稱
  • getcontent:取得整個看板的資料,包含裡面的Workflow
  • leave:離開看板


▲圖2:ezKanban系統為了操作Board所設計的多個Presenter與View Model


圖3為CreateBoardViewModel、RenameBoardViewModel以及DeleteBoardViewModel的內容,長得一模一樣,都只包含boardId這個資料。


▲圖3:三個View Model內容都很像


圖4的BoardContentViewModel長得和其他人不一樣,有比較多資料。


▲圖4:BoardContentViewModel

***

區分Command和Query

Command是會修改系統狀態但不回傳資料的操作,Query是不會改變系統狀態但會回傳資料的操作。Command query separation(CQS)是由Bertrand Meyer所提出的設計方法,最早應用於他自己發明的Eiffel語言中。

Command query responsibility segregation (CQRS) 受到CQS的影響,將這個概念套用在軟體架構上。

請注意看圖2裡面關於Board的操作,可以套用CQRS的精神,將使用案例分成兩大類:

  • Command:addmember、changetitle、create、delete、enter、leave
  • Query:get、getcontent

套用CQRS之後理想狀況是所有的Command共用相同的Output、Presenter以及View Model。雖然理想上Command不應該回傳任何值,但實務上在分散式系統中,可以讓Command回傳有限的值,例如所產生物件的id,以及執行的結果(成功或失敗)與錯誤訊息。


▲圖5:可以給Command共用的Presenter與View Model


▲圖6:簡化後Board功能只需要兩個給Query使用的Presenter與View Model

***

友藏內心獨白:Clean Architecture + CQRS = 好棒棒。

2020年8月19日 星期三

再談Clean Architecture三原則

August 19 08:45~10:00;12:56~13:38


2018年Teddy寫了幾篇介紹Clean Architecture的文章如下,其中有三篇提到Clean Architecture三原則:分層相依性跨層,今天再一次一起討論這三個原則。

***

分層原則

如圖1所示,Clean Architecture有四層:Entities、Use Cases、Interface Adapters、Frameworks & Drivers。分層原則乍看之下最簡單也最沒爭議,但實際上有些物件要放在哪一層還是存在灰色地帶。

最核心的Entities存放domain model物件。如果對照領域驅動設計(Domain-Driven Design;DDD),Aggregate、Entity、Value Object、Domain Service就放在這一層。應用程式邏輯放在Use Cases這一層,也就是DDD裡面的Application Service。

先看這兩層就好,請問DDD的Repository要放在哪裡?大部分的人覺得要放在Use Cases這一層,但也有人主張要放在Entities。所以,雖然Clean Architecture這四層大方向來講區分得很清楚,但在實作的時候有一些「支援物件」,像是剛剛提到的Repository以及Mapper等實際上要放在哪裡,還是有一些討論的空間。


CleanArchitecture

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

***

相依性原則

原始碼的依賴方向必須由外往內、由低層往高層。也就是說Entities層的物件只可以參考(依賴)同一層的其他物件,不可以參考其他層的物件。Use Cases層只可以往內參考Entities層以及自己這一層的物件,不可以往外參考其他層的物件,依此類推。

但是,如果Use Cases需要將Aggregates儲存到資料庫,那一定要參考到最外層的資料庫物件啊,這怎麼辦?如圖2所示,Clean Architecture透過依賴反轉(Dependency Inversion Principle;DIP)確保相依性原則可以被遵守。


▲圖2:透過依賴反轉做到相依性由外往內。

現在問題來了,圖2的Workflow aggregate透過AddWorkflowUseCase把Workflow儲存到MySQL資料庫,這樣子有沒有違反相依性原則?

雖然SqlWorkflowRepository與MySQL都跨層直接參考Workflow,但相依性的方向還是由外向內,因此並沒有違反相依性原則。但是,這麼做違反了Clean Architecture的第三個原則:跨層原則。

相依性原則要完全遵守也是不容易的一件事,例如Entities層的Aggregate要發Domain Event(領域事件)需要用到event bus公具程式,最簡單的方式是找人家已經寫好的工具來使用,但是這樣就違反了相依性原則。為了滿足相依性原則,就要幫著個event bus工具程式定義介面,透過依賴反轉做到相依性原則。有時候定義依賴反轉介面很簡單,但有時候卻不容易。

***

跨層原則

當Entities層的物件跨越離開Use Case層往外傳遞時(傳到UI或是DB或其他外部系統),不要直接把Entities層的物件傳出Use Case層,而是要在Use Cases層定義往外傳遞的介面與資料結構。簡單說就是把Entities層的物件轉成DTO(Data Transfer Object)再往外傳遞。

如果直接把Entities層的物件往外傳,例如直接傳給UI,很有可能因為UI端的需求,導致回頭影響Entities層的物件。也就是說,presentation logic影響了domain logic。為了做到分層負責與單一責任,所以物件跨層的時候要轉一次資料型態。

***

潔癖

完全做到Clean Architecture的三個原則,系統架構就會變得很乾淨,但實作上卻會很麻煩。就好像跟一個有潔癖的人住在一起,居住環境一定很乾淨,但你可能會被對方煩死。一天要擦三次地板、衣服髒了要馬上洗、裝冷飲的杯子要用杯墊,不可以讓水滴到桌上、回家要馬上消毒等等。

這幾年實作Clean Architecture的經驗,Teddy覺得只要做到以下幾點即可,其餘部分可以不用那麼「乾淨」:

  • 相依性原則在大部分的情況下都要遵守,特別是Entities層與Use Case層。如果真的需要使用到一些工具軟體且又不能不想透過依賴反轉滿足相依性原則,這樣的特例不能太多,且要自己心裡有數,承受這樣的依賴關係。
  • Interface Adapters層經常與框架緊密相關,例如Web Controller和所使用的Web Container框架緊密相關。在這種情況下,要做的完全的依賴反轉是很痛苦的一件事。所以Interface Adapters層放生,就讓它和框架緊密耦合吧。
  • 把Use Cases分成Command與Query可以簡化很多Interface Adapters層的物件設計。Command是會改變系統狀態但不回傳值(或只回傳物件的id等特定資料)的操作,Query是回傳資料但不會改變系統狀態的操作。基本上Command可以共用同一個Presenter與View Model,只有Query需要客製化的Presenter和View Model。區分Command和Query之後,Interface Adapters層可以清爽許多。

***

友藏內心獨白:沒有絕對的乾淨,無塵室也是有灰塵。

2020年8月18日 星期二

領域驅動設計學習筆記(10):實作Repository

August 18 10:00~13:35


背景介紹

在《Clean Architecture》書中建議使用Gateway來存取物件,透過依賴反轉原則,避免領域物件(Domain Model,在Clean Architecture中稱為Entity Layer)與使用案例層對資料存取層產生直接依賴。

Gateway是一個比較抽象的名詞,Teddy在實作Clean Architecture的同時也套用了領域驅動設計(Domain-Driven Design;DDD),因此採用領域驅動設計所建議的Repository設計模式來實作Gateway。

在DDD中,每一個Aggregate會對應到一個Repository負責物件持久化的工作。

接下來討論幾種實作Repository的方法,以及各自的優缺點。

***

Take 1,一對一

實作Repository最直接的方式就是先幫每一個Aggregate宣告一個相對應的Repository介面,然後實作這些介面。圖1為Clean Kanban系統的三個Repository(Teddy開發的看板系統)。

▲圖1:一個Aggregate對應一個Repository

Teddy幫每一個Repository提供兩個版本的實作物件:

  • InMemoryRepository:把Aggregarte儲存在記憶體中,方便測試使用,如圖2所示。
  • SerializableRepository:透過Java的serialization功能直接把物件儲存在檔案中,如圖3所示。

▲圖2:ImMemoryBoardRepository


▲圖3:SerializableBoardRepository

第一種方法最直接也很簡單,相信大家都看得懂。

***

Take 2,泛型

看到這裡鄉民們應該會想:「每一個Aggregate都宣告一個,Repository介面,這樣太麻煩了,怎麼不用泛型(generic programming)?」


▲圖4:採用泛型Repository介面


圖4為採用泛型的Repository介面,之後只要需一個InMemoryRepository實作便可讓所有的Aggregate使用,如圖5所示。

▲圖5:採用泛型的InMemoryRepository

***

Take 3,使用JPA

前兩種方式都需要自己撰寫實作Repository的程式碼,如果想偷懶使用Java 持久化 API (Java Persistence API;JPA),讓Hibernate自動產生Repository實作。


▲圖6:使用JPA讓SpringBoot透過Hibernate自動產生Repository實作

這個做法省去了自己撰寫操作資料庫程式的麻煩,但存在三個問題:

  • 有些資料庫操作沒有定義在標準的Repository介面裡面,需要自己撰寫Java持久化查詢語言 (Java Persistence Query Language;JPQL)客製化查詢條件。例如圖6的WorkflowRepository繼承自CrudRepository介面,而CrudRepository只有findById,可以透過workflow id 找到workflow。如果需要透過board id找到workflow,就需要自己增加一個getWorkflowsById的方法,然後用@Query annotation來撰寫自訂查詢條件。這原本也不是什麼大問題,但如果查詢結果無法完全用JPQL表達,而是需要在從資料庫回傳資料後再透過Repository處理,那就沒辦法了(因為WorkflowRepository只是一個interface,它的實作是程式執行時動態產生)。
  • 第二個問題來自於Clean Architecture,Repository儲存的對象是Aggregate,也就是圖6中的Workflow。但直接把位於Entity Layer的Workflow物件丟到最外層的DB & Drivers,違反了Clean Architecture的跨層原則。
  • 第三個問題也是因為套用Clean Architecture才會有的困擾。為了讓JPA自動產生資料庫對應程式碼,必須在Entity(domain model裡面的entity類別)貼上JPA的@Entity annotation,如圖7所示。雖然@Entity是JPA標準的annotation,但JDK並沒有內建這些annotation。為了使用它必須要加入對框架的相依性,例如要引用Hibernate。如此一來,便違反了Clean Architecture的相依性原則:「高層不可以相依於底層(原始碼的相依性只可由外往內,由底層往高層)」。此外,在domain model的entity類別身上貼了一堆「狗皮膏藥(annotation)」,對於有潔癖的人來講,也是無法接受的一件事XD。


▲圖7:貼上@Entity讓JPA知道這是一個需要持久化的物件

***

Take 4,Bridge設計模式

讓JPA直接產生Repository實作雖然方便,但卻少了一些自行撰寫程式的彈性,請參考圖8。

▲圖8:Client直接相依於Repository介面


這個問題的根本原因是我們把JPA所產生的Repository直接丟給客戶端使用,如果可以在客戶端和Repository之間增加一個間接層,就可以透過這個間接層手動增加一些彈性的操作,請參考圖9。這個設計,就是Bridge設計模式。


▲圖9:在Client與JPA產生的Repository實作之間增加一層。原本的JPA Repository改名為RepositoryPeer,客戶端對應的是新增加的Repository介面。

***

最後實作程式碼如圖10所示。

▲圖10:套用Bridge設計模式之後的Repository。


眼尖的鄉民可能注意到圖10中的一些細節:

  • WorkflowRepository所儲存的物件類別是Workflow,但透過WorkflowRepositoryPeer儲存到資料庫裡面的是WorkflowData。這是為了不要將Entity層的Workflow直接傳到第四層的Database & Driver。
  • 為了把Workflow轉成WorkflowData,以及反之把WorkflowData轉成Workflow,還套用了Mapper設計模式。這個資料傳換由WorkflowRepositoryImpl所負責。這也是套用Bridge的好處,在呼叫peer之前以及之後,可以插入一些程式邏輯。
  • 因為最後儲存到資料庫的是WorkflowData類別,因此只要在WorkflowData類別貼JPA annotation即可(參考圖11),位於entity 層的Workflow保持乾淨的原貌。


▲圖10:WorkflowData類別

***

友藏內心獨白:終於寫完了。

2020年7月17日 星期五

領域驅動設計學習筆記(9):Query和Read Model

July 17 12:40~13:48

▲圖1:解釋所有事情的一張圖,取自《Introducing EventStorming


背景介紹

在Alberto Brandolini的《Introducing EventStorming》書中,有一張圖用來解釋他對於採用event storming塑模問題的想法,如圖1。


▲圖2:解釋所有事情的一張圖,取自《Introducing EventStorming


把圖1用event storming展開,出現如圖2所示的流程:

  1. 使用者執行Aggregate身上的Command。例如,在看板系統中,使用者新增看板(Create Board)、移動卡片(Move Card)。
  2. Aggregate執行完Command後發出Domain Event,用來代表系統狀態發生改變。例如看板已新增(BoardCreated)、卡片已移動(CardMoved),
  3. Domain Event也可能由External System所產生,例如,收到銀行傳來的付款確認(PaymentConfirmed)事件。
  4. 剛剛上述行為屬於Write Model,也就是執行Command改變系統狀態並產生領域事件。但使用者不只會改變系統狀態,也需要讀取系統資訊,以決定要執行哪一個Command。表達讀取資訊的模型稱為Read Model,它可以從改變狀態的領域事件而轉換出Read Model。
  5. 將這個Read Model傳給UI,可以做出最後使用者看到的系統畫面。
  6. 使用者看到系統畫面,或更抽象的表示,看到Read Model,從而做出決定,執行下一個Command。
  7. 圖2中的Policy表示發生領域事件之後需要後續處理的事情。例如,當使用者註冊成功,則寄發啟動帳戶的email。

***

Event Storming + Clean Architecture

▲圖3:cleanKanban範例,將Read Model簡化成Use Case的Input


Teddy在上〈領域驅動設計與簡潔架構入門實作班〉教導學員用Event Storming來建立Domain Model與Ubiquitous Language。在實作面,套用Clean Architecture,並採用TDD/SBE的方式來撰寫程式。因為Clean Architecture最核心之處在於Entity Layer與Use Case Layer,把UI和DB都當成技術細節,因此在塑模時先不考慮。

所以Teddy把原本Event Storming的綠色便利貼,由原本的Read Model,簡化為使用者看完Read Model之後,決定要執行哪一個Command所給予的參數。例如,顧客去麥當勞櫃台點餐,他看到Menu上面有1號餐、2號餐、N號餐等,這個Menu就是Read Model。最後使用者決定要點一份5號餐,外加一份大薯條,這些資料就是Input。

***

加上Query

如果只是實作後端,從Clean Architecture的角度來看,上面這些分別代表Command、Aggregate、Domain Event、Policy、Read Model(Input)以及UI 的便利貼,也就夠了。但實際開發完整的軟體需要接上前端,此時發現少了一種便利貼:Query

Command是會改變系統狀態不傳回值的操作,Query則相反,不會改變系統狀態但會回傳值。圖1與圖2中,Domain Event—>Read Model之間,應該還需要補上Query,這樣子參考Event Storming轉成程式碼的時候就可以有一對一的對應關係,如圖4所示。


▲圖4:ezKanban範例,由Query (Get Home Content) 產生Read Model (或稱為View Model),再將此Read Model顯示在UI上。


在圖4中,Query並不像Command需要傳給Aggregate,其實作方式可以透過撰寫Read Repository以及搭配Projection (將原本適合用來寫入的Write Model資料映射成適合讀取的Read Model資料)。

***

結論

加上Query之後,整個Event Storming的便利貼就更完整,也可以和UX/UI設計師討論人機互動的問題。

但是,加上Query與UI,整個模型也變得更加複雜。從軟體架構的角度來看,是否在軟體開發初期就需要增加這樣的複雜度,也是一個可以討論的議題。當然可以只針對Core Domain以及支援商業流程的主要Read Model優先設計畫面,其他次要部份採用迭代與增量的方式來實作,也是一種選擇。

***

友藏內心獨白:Model 不是越詳細越好,只要能表達problem domain我們所關心的問題就好。

2020年7月7日 星期二

領域驅動設計學習筆記(8):幫Event Handler取個好命字

July 07 13:32~14:50

▲幫Event Handler取個好名字


最終一致性(Eventual Consistency)

領域驅動設計(Domain-Driven Design;DDD)引入Aggregate,更新同一個Aggregate需要保證交易處裡的一致性達到ACID要求。跨Aggregate之間則是透過領域事件通知,達到最終一致性(eventual consistency)即可。


▲圖1:Board與Workflow兩個Aggregate


以cleanKanban系統為例,圖1表示兩個Aggregate—Board與Workflow的關係,一個Board擁有零到多個Workflow。因為兩者是獨立的Aggregate,當新增Workflow之後,Board需要收到WorkflowCreated領域事件通知,才能夠建立它與Workflow之間的關係,如圖2所示。


▲圖2:Aggregate之間透過領域事件更新狀態


***

取名字

圖2是Event Storming(事件風暴)產出圖表,Event Storming定義了Domain Event、Command、Aggregate、Read Model、Policy、External System等便利貼,但是沒有代表處裡事件的Event Handler。

基本上Event Handler是一種程式實作細節,是落實Policy的實作方式。為了落實DDD的ubiquitous language in code,Teddy希望Event Storming也可以用表達Event Handler的便利貼,以便於實作時可以看著Event Storming圖表來寫程式碼。


▲圖3:新增不同顏色便利貼並直接用EventHandler當名字


Teddy一開始嘗試增加一種新顏色的便利貼,並把它貼在要透過Domain Event達到最終一致性的兩個Aggregate之間。如圖3所示,WorkflowEventHandler負責聽取WorkflowCreated領域事件,然後呼叫Commit Workflow使用案例建立起Workflow與Board之間的關係。

現在試著讀一下這個模型的通用語言:


Teddy執行Create Workflow使用案例,執行完畢後產生Workflow Created領域事件。WorkflowEventHandler一收到Workflow Created,執行Commit Workflow建立剛剛新增的Workflow與它所屬Board的關聯。