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層可以清爽許多。

***

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