Quantcast
Channel: 搞笑談軟工
Viewing all 1089 articles
Browse latest View live

領域驅動設計學習筆記(21):貧血模型與充血模型(上)

$
0
0

September 09 07:29~8:30

▲Pascal這種身材應該算是充血模型,把房子都壓垮了還不充血嗎 XD


緣起

幾天前在臉書DDD台灣社團看到有人問「貧血模型」與「充血模型」的問題,問題大意如下:「假設有一個Product Entity,針對該Entity實作查詢產品功能。」

方法一:將查詢功能做在Product身上

public class Product {

private final ProductRepository repository;

public Product (ProductRepository repository) {

    this.repository = repository;

}

public Optional<Product> getProductByName(String productName) {

      return repository.findByName(productName);

}

}

***

方法二:將查詢功能做在Use Case身上

public class GetProductUseCase {

private ProductRepository repository;

public GetProductUseCase (ProductRepository repository) {

    this.repository = repository;

}

public void execute(GetProductInput input, GetProductOutout output) {

      output.setProdeuct(repository.findByName(input.getProductName()));

}

}

***

發問者認為:

  • 方法一的Product有getProductByName這個「行為」,感覺符合充血模型(Rich Domain Model)的定義。但是,讓Product直接耦合Repository好像又不太對。
  • 方法二將getProductByName從Product身上拔除,升等為GetProductUseCase,拿掉Product對Repository的依賴。但是,如此一來Product身上就光溜溜沒有行為,變成Fowler所說的貧血模型(Anemic Domain Model)

到底要怎麼做比較好?

***

不是貧血、充血的問題

關於上述問題,Teddy覺得發問者所舉的例子本質上和Product Entity屬於充血模型或是貧血模型並無直接關係,而是和CQRS(Command Query Responsibility Segregation;命令查詢職責分離)比較有關。

在方法一的所謂充血模型範例中,Product身上的getProductByName(String productName)方法,是一個Query(查詢,回傳資料但不會改變系統狀態的操作),它本來就不需要也不應該存在Product身上。從CQRS的角度來看,Entity主要是要表達Command(命令,會改變系統狀態但不會回傳值的操作),因為發問者把Command與Query耦合在Entity身上,才會有「將getProductByName放在Product身上讓Product產生對Repository的依賴」的困擾。

方法二將getProductByName升級成GetProductUseCase,它現在變成一個代表Query的使用案例,如此一來實現了CQRS。把getProductByName從Product身上拔掉,並不會讓Product變成貧血模式,反而可以讓Product專心去負責原本應該表達的「Command Model(或稱為Write Model)」。這才是DDD(領域驅動設計)要套用的地方,應用在Write Model,而不是在Read Model。

在發問者所舉的例子中,Product就只有Query而有沒任何Command,所以將Product的Query拔掉之後,Product身上就沒有其他行為了,才會誤認此時的Product變成了貧血模型。

***

本來無一物

以上情境,讓Teddy想起有一次聽 Kevlin Henney 的演講,他提到一個故事(以下為Google翻譯後經Teddy修改過的中文):.

尊貴的 Qc Na 大師與他的學生 Anton 同行。Anton希望能引起師父的討論,他說:「師父,我聽說物件是很好的東西——這是真的嗎?」Qc Na憐憫地看著自己的學生,答道:「笨學生——物件只是窮人的閉包(closures)。」

被打槍之後,Anton離開了他的師父,回到了他的小研究室,打算研究閉包。他仔細閱讀了整個「Lambda:The Ultimate...」系列論文及其同類論文,並實作了一個帶有基於閉包(closure-based)的物件系統的小型 Scheme 直譯器。他學到了很多東西,期待著告訴師父關於他的進步。

在與 Qc Na 的下一次散步中,Anton 試圖給他的師父留下深刻印象,他說:「師父,我仔細研究了這件事,現在明白物件確實是窮人的閉包。」 Qc Na 的回應是用棍子打Anton,說:「你什麼時候才學得會?閉包是窮人的物件。」

那一刻,Anton開悟了。

Object is a poor man’s closure, and closure is a poor man’s object.

***

下集待續

先講完背景故事,下一集Teddy從DCI(Data, Context, Interaction)的角度,再談貧血模型與充血模型的不同觀點。

***

友藏內心獨白:貧血模型是富人的充血模型,充血模型是富人的貧血模型 。


領域驅動設計學習筆記(22):貧血模型與充血模型(下)

$
0
0

September 10 03:38~04:58

▲ezKanban的Board Aggregate套用DCI之後,可以動態指派新的角色給它,以便獲得新的行為能力。


阿嬤的物件導向

DCI架構(Data, Context, Interaction)由Trygve Reenskaug和James O. Coplien所提出,這兩位都是軟體界的名人,前者是MVC(Model-View-Controller)架構發明人,後者是模式與敏捷領域的大師。Teddy今年6月曾介紹過DCI並應用它來重構DDD Aggregate:

***

傳統認為好的領域模型應該設計成充血模型(Rich Domain Model),物件身上封裝著資料與操作該資料的商業邏輯,程式比較容易閱讀與維護。貧血模型(Anemic Domain Model)則被認為是不好的物件導向設計,它將商業邏輯從物件身上抽離出來,物件行為被降級成單純的資料庫操作,因此導致商業邏輯不清、系統難以維護。

Coplien 在介紹DCI的演講中曾說過:「傳統物件導向將資料與行為封裝在一起的做法,是你阿公時代的物件導向技術。」他建議採用DCI架構,將系統區分為DataContextInteraction。DCI架構主張物件的行為,必須要放在特定的使用案例(Use Case)裡面來解讀,才能夠理解其意義。

舉個例子,「人」這個物件,具備「唱歌」的行為。但當一個人在唱歌的時候,到底代表什麼意義,必須從使用案例中去理解。你在KTV唱歌、在學校朝會唱歌、在當兵的時候唱歌、在求婚的時候唱歌、在失戀的時候唱歌、在遊覽車上唱歌、在無人的海邊唱歌、在選秀節目中唱歌、在監獄裡面唱歌,意義可能完全不同。在DCI架構中,將這些不同的使用案例稱為Context(情境、脈絡、上下文)

人這個物件,只需具備資料(Data)即可。至於人到底具備哪些行為,則是在某一個特定Context中,由Context將所需行為動態「注入」到人身上。一個Context經常需要協調多個物件一起工作,這個協調的動作在DCI架構中就稱為Interaction(互動)。

***

何處惹塵埃

上一集,Teddy將「Object is a poor man’s closure, and closure is a poor man’s object.」改寫成「貧血模型是富人的充血模型,充血模型是富人的貧血模型 。」不少鄉民不知道Teddy在瞎說什麼。

為什麼說貧血模型是富人的充血模型?從DCI(富人)的角度來看,貧血模型就是資料物件(Data Object),物件的真正行為延後到執行期間再由Context指派給它。所以,你希望物件有什麼行為,沒問題,爸爸(Context)都可以指派給你。這麼豐富的行為,領域物件還能不「充血」嗎!

為什麼說充血模型是富人的貧血模型?充血模型最佳代表,就是DDD的領域模型(Domain Model)。在DDD中,領域模型主要由一堆充滿行為的Entity、Domain Service、Value Object所構成。其中若干個Entity與Value Object依據商業規則被迫「群聚在一起」形成Aggregate(聚合),並從中找一個Entity出來當「大股頭」(Aggregate Root)號令這個「部落」(聚合)。

客戶端(漢人、老外)只可透過Aggregate Root存取內部的Entities或Value Objects,因此Aggregate Root身上會有很多行為,更是充血到爆。為什麼Teddy會說這些充滿行為的領域模型是「富人的貧血模型」?因為這些行為都是在編譯期間(Compile Time)就已經決定了,因此雖然從傳統物件導向的角度來看它們的血量都很足夠,但從DCI(富人)的角度來看,它們也只是另類的貧血模型罷了。

***

結論

世事無絕對,端看造化而定。

***

友藏內心獨白: 統領埔到底要租給誰,搞得我好亂啊 XD。

Clean Architecture是一種過度設計嗎?

$
0
0

September 13 11:24~11:46

▲恰如其分的設計,還是過度設計?


有一位學員問Teddy…

學員:Clean Architecture是一種過度設計嗎?

Teddy:怎麼會這樣想?

學員:我跟同事討論設計問題,同事覺得我們系統不大,如果套了Clean Architecture,這樣來看Clean Architecture算不算是一種過度設計(Over Design)?

Teddy:你知道Mac Pro嗎?

學員:知道。

Teddy:如果買到頂級的Mac Pro,一台要價超過一百六十萬台幣。這麼高的規格,請問你可以說Mac Pro是一種過度設計嗎?

學員:嗯……

Teddy:如果你買了一台頂規的Mac Pro,只是拿它來上上網,寫寫小程式,在這種情境之下你可以說這是一種「過度消費」,因為你買了你用不到的計算能力。但如果你是專業的影音剪輯人士,一台一百多萬的Mac Pro對你而言可能只是剛剛好而已,甚至還會覺得不足以應付你的工作。

***

Clean Architecture是一種過度設計嗎?

領域驅動設計是一種過度設計嗎?

Design Patterns是一種過度設計嗎?

微服務是一種過度設計嗎?

陶朱隱園是一種過度設計嗎?

高鐵是一種過度設計嗎?

以上皆非,它們都只是一種「設計」,至於這種設計是不是過度或是恰如其分,則是要看應用的情境(Context)而定。

一個事物的含意,必須放在特定的情境去解讀才有意義,才可以避免產生誤會。

***

友藏內心獨白:這就是DCI。

有沒有發出聲音?

$
0
0

September 14 10:25~11:16

▲電影《達摩》畫面


幾年前讀了某本與禪學有關的書,書中提到一個公案:

「在深山中有一片茂密的森林,一棵大樹倒下,發出巨大聲響,但沒有任何人聽到。請問這棵樹有發出聲音嗎?」

***

梁武帝

梁武帝是一位篤信佛教的皇帝,在電影《達摩》中他與達摩會面,他問達摩:

梁武帝:「自朕登基以來,修佛寺,造佛像,抄寫經卷,供養僧侶無數。敢問大師,朕有何功德?

達摩:「無功無德。」

***

一棵大樹倒下,它不甘寂寞,怕沒有人知道它輝煌的一生,所以問了大師自己有何功德?

在電影中志公大師對梁武帝說:「皇上本有功德,但常掛口中,要人稱讚,便是刻意貪圖功德。善惡抵銷,也就沒有功德啦!」

***

政客

很多時候,政客為了自己的政治利益,大撒幣搞大內宣,明明沒有任何樹倒下硬是要誇稱自己做了什麼偉大的事。例如:台灣政府購買的疫苗數量已經夠了、廣告說明是劉德華演唱會,現場來的卻是蘆洲劉德華、演講題目是介紹DDD,但實際內容卻是資料庫驅動設計。

正所謂三人成虎,這種似是而非的大內宣,講久了還真的可能有人會相信。反正政客服務的對象原本就不是全體國民,只要能夠騙到「韭菜」就可以了。

梁武帝至少還做了點什麼,但政客型的人,光靠一張嘴騙選票,刻意貪圖功德的本領遠勝於梁武帝。

***

什麼是真的?

什麼是真理?這是一個很困難的哲學問題。樹的確倒下了,但世界上沒有人發現。從人的角度來看,並未觀察的樹倒下,不知道的事情等於不存在,這也是很合理的觀點。

對樹而言,我倒下了有必要到處宣傳讓人知道嗎?

你想要成為怎樣的樹?

***

友藏內心獨白:Quality Without A Name。

成本與價值

$
0
0

September 15 18:36~19:02

▲ezKanban 團隊mobbing實況


學員:如何讓整個團隊,包含剛進來的新人,都可以學會用DDD(領域驅動設計)開發軟體?

Teddy:我有一個很簡單且有效的方法,但這個方法就算你知道了你們公司應該也不會採用。

學員:什麼方法?

Teddy:Mobbing,又稱為Mob Programming,整個團隊,包含測試、UI/UX與Product Owner,一起開發軟體。

學員:類似Pair Programming嗎?

Teddy:對,不過是Pair Programming的加強版,不只是兩個人一起開發,而是整個團隊。

學員:這樣的確是很難在公司推行,Pair Programming老闆都覺得成本太高

Teddy:那你們都怎麼帶新人?

學員:我們會指派一位導師帶著新人做一陣子。

Teddy:然後呢?是不是一、兩個禮拜之後就讓新人單飛,然後每天問他進度?

學員:(苦笑)沒有這麼慘啦,有問題還是會回答。

Teddy:軟體開發的成本,其實很難量化。老闆通常只看到「開發人員」的成本,因為這是最直接的成本。但除了直接寫code以外,其他的成本呢?

Teddy:你要不要Code Review?有bug要不要改?要改多久?做出來的東西客戶不要怎麼辦?舊員工離職新員工交接怎麼辦?軟體搞到變成硬體,改不動也沒人敢改怎麼辦?後端、前端、內人、外人互相等對方工作完成怎麼辦?需求不清楚開發人員亂寫怎麼辦?測試團隊自創需求,胡亂回報issues怎麼辦?

Teddy:從精實開發的角度來看,這些都是浪費。很多的等待(延遲)、重工、半成品、過度生產、交接、缺陷、工作切換、重複學習。我不敢說Mobbing可以完全消除這七種浪費,但以我的經驗,這種「看起來成本太高」的開發模式反而能夠消除浪費而降低成本並提高產品的品質與價值。

Teddy:老闆覺得開發成本太高,會不會是因為自身的產品在市場上的價值太低,或是想要犧牲品質來拉低成本,所以任何提高品質的活動都會被認為是增加成本。不管如何,這有可能是對於自己產品的定位不同,產生的價值觀落差。

***

沒有銀子彈,但是有高科技武器跟二戰時的武器之分。

***

友藏內心獨白:知道了也做不到。

頭痛醫腳,腳痛醫頭

$
0
0

Oct. 05 17:24~18:55


幾年前有一陣子Teddy的左腳腳底板只要踩到地上就好痛,觀察了幾天都沒改善,查了網路資料,感覺很像足底筋膜炎。找了一天去看家裡附近的的復健科整所,把症狀告訴醫生之後,Teddy跟醫生說:「我覺得我好像是足底筋膜炎」。

醫生聽完之後面帶微笑,要Teddy擺出幾個姿勢,然後說:「你要拉腰。」

明明是腳底痛,為什麼要去拉腰?

醫生說:「你這是腰椎突出影響到腳,我看過很多類似的病人,他們也都以為是腳有問題。但這個現象一般來講只要拉腰就可以改善。」

Teddy半信半疑之下持續去復健拉腰,沒想到後來腳底板疼痛的問題就消失了。

***

最近有一位客戶打電話給Teddy…

Teddy:泰迪軟體您好,我是Teddy。

客戶:Teddy老師你好,我們是XX公司。

客戶:請問你們有單元測試的企業內訓課程嗎?

Teddy:有啊,泰迪軟體所有公開班課程都提供企業內訓。

客戶:是這樣,最近我們的系統出了點包,公司想要提升軟體品質,長期來講要做持續整合與持續佈署,短期想先從測試開始。

Teddy:這很合理,一般人想到提升品質、減少bug,直接就想到測試。

客戶:所以我們從自動化單元測試著手是OK的?

Teddy:從測試著手有兩個方向,第一個是人工測試,雖然看起來很笨,但只要有錢,這是最簡單又立即可以看到效果的方法,但長遠來講會不符成本,畢竟用人工方式做回歸測試不只成本高,速度也慢。

Teddy:如果從自動化單元測試著手,不是說員工學會測試技巧就可以快快樂樂寫出有效的單元測試。依據我的經驗,很多人不寫單元測試,或是說寫不出單元測試,不是因為測試太難,而是因為設計太爛

Teddy:因為設計太爛,所以耦合太高,系統很難獨立測試。這時候,可能需要先教同仁基本的物件導向設計觀念與SOLID等原則,更進階要學習Design Patterns與Clean Architecture。軟體設計品質提升,測試自然變得比較容易。

***

軟體品質不好,不一定直接從測試著手。有沒有可能是需求一開始就錯了?或是規格沒弄清楚?如果是這種狀況,也許要先學Event Storming、Specification By Example與Design By Contract。有沒有可能是設計過於複雜導致錯誤?如果是,應該先改善設計品質,讓設計簡單到不會出現明顯的錯誤。

設計與測試是一體兩面,設計能力好的人,通常測試也做得好,反之亦然。

***

友藏內心獨白:兩點之間最短距離通常不是直線。

無痛將驗收測試文件寫在測試案例中

$
0
0

Dec. 23 20:18~21:45

截圖 2021-12-23 下午9.42.42

▲新買的電腦還沒寫code先來寫部落格

 

前情提要

2017年Teddy寫過一系列關於行為驅動開發(Behavior-Driven Development,BDD)的文章,請參考以下列表:

  1. BDD(1):詳盡的文件就是可用的軟體
  2. BDD(2):大家來吃小黃瓜之Cucumber運作原理
  3. BDD(3):在Eclipse執行Cucumber-JVM
  4. BDD(4):第一個Cucumber-JVM範例,上集
  5. BDD(5):第一個Cucumber-JVM範例,下集
  6. 在IntelliJ IDEA使用Cucumber(上)
  7. 在IntelliJ IDEA使用Cucumber(下)
  8. BDD(6):讓Step找到Step Definition
  9. BDD(7):使用Transform讓稅金同時支援5%和0.05表達方式
  10. BDD(8):實作第一個開發票Scenario
  11. 在IntelliJ IDEA使用Cucumber(上)
  12. 在IntelliJ IDEA使用Cucumber(下)

 

當時學了一陣子Cucumber但後來一直沒有真正使用它。主要原因在於Teddy覺得「Cucumber將需求寫在feature file裡面,然後再轉成step definition程式碼,然後開發人員在這些step definition立面撰寫真正的測試邏輯」的這種流程不太流暢。完整開發一個功能,從寫feature file到寫完produciton code的過程會一直撞牆(卡住),尤其是feature file有參數要傳給step definition,真的不太直覺。

但當時Teddy也沒多想,就把這個問題放著。

***

活文件

前幾天讀了《Living Documentation: Continuous Knowledge Sharing by Design》,書中有一個例子如下圖所示:

 

截圖 2021-12-23 下午8.59.19

▲圖1:Living Documentation書中範例

 

看到這個例子Teddy突然心中有感:「對了,就是這樣。應該要把Cucumber的feature file內容寫在程式碼中而不是與程式碼分離。」於是Teddy也試著修改ezKanban的測試案例看看效果如何,請參考下圖:

 

截圖 2021-12-23 下午9.26.24

▲圖2:修改後的ezKanban驗收測試案例

 

Teddy覺得直接在測試案例程式碼加上Given-When-Than說明文字讓測試案例清楚很多,也免去了原本Cucumber在feature file與step definition之間做binding的麻煩。

至於圖2中Scenario(), When(), WhenFailure()是怎麼來的?其實很簡單,原本Teddy以為書中用了什麼工具,但找了一下沒找到。後來想到,自己寫一個不就好了。程式很簡單,長成下面這樣:

截圖 2021-12-23 下午9.34.27

▲圖3:用來在測試案例中撰寫Given-When-Than說明文字的工具,算是一的超級簡單的DSL(domain specific language)

 

這種做法,與程式語言和工具都無關。絕大部分的高階語言要寫出圖3中的程式應該是幾分鐘就搞定的事。

 

***

 

友藏內心獨白:本篇在新買的第12代i9電腦上撰寫XD。

2021年終心得:Coding & Learning

$
0
0

Dec. 31 20:34~22:08

截圖 2021-12-27 下午3.59.13

 

上次寫年終心得是〈2019年終心得:突破〉,這兩年疫情爆發,也發生了不少事,加上晚上有空,寫個2021年回顧好了。
今年的心得可以用Coding & Learning兩個字來概括。

2021年泰迪軟體的重大事件有:

***

業績

今年泰迪軟體的營業額是成立以來最高的一年,比2019年的高點還要多一點點。雖然因為疫情緣故很多傳統的課程像是Scrum、Design Patterns公開班今年都只開了一次,重構與測試的公開班課程則是完全停開,但受惠於【領域驅動設計與簡潔架構入門實作班】頗受鄉民歡迎,開了好幾梯次的公開班與企業內訓,反倒是增加不少收入。

但【領域驅動設計與簡潔架構入門實作班】之所以受歡迎,源自Teddy在2019年說過的一段話:「研究許久的Clean Architecture(簡潔架構)Domain-Driven Design(領域驅動設計),今年結合了這兩者,發現無論是在架構上、problem modeling以及coding,軟體開發與維護變得比較容易也更系統化。」

加上從2020年6月底開始,Teddy與北科大軟體系統實驗室的ezKanban團隊開始mobbing,至今花了超過1500小時的時間一起開發ezKanban。在這個過程中大量套用DDD、Event Storming、Clean Architecture、Event Sourcing、CQRS、TDD、Design by Contract(DBC)、Exception Handling、Specification by Example(SBE)、Living Documentation、Continuous Integration、Refactoring等方法,幾乎快把Teddy畢生所學都用在上面。

Teddy很喜歡Kent Beck說過的一句話:「If you stop coding, you stop learning」身為一位軟體開發從業人員,不管到了什麼階段,還是應該持續保持寫程式的習慣。寫程式的目的不一定是要拚production code的數量,而是要從中學習。這種學習的深度,會與所寫的程式數量成正比。寫得越多,領得越多。啊,不對,是寫得越多,學得越多。

今年七月ezKanban幾位研二學生畢業之後團隊只剩下3位學生,後來OIS團隊加入,增加了一搏、二碩、三大,一共六個人。感謝這幾位學生今年和Teddy一起mobbing,度過許多美好的coding時光。

***

畢業

2019年Teddy提到Erica已經能夠獨立作業,在敏捷教練、引導、敏捷需求管理方面也有不錯的成績。今年9月底Erica從泰迪軟體畢業,回到她的家鄉生活與工作。

泰迪軟體一開始就只有「Teddy」一個人,Erica加入之後,幫了很多忙,很多事情是Teddy能力範圍以外,剛好是Erica比較擅長,兩人互補。Erica剛到泰迪軟體的時候,Teddy告訴她:「我沒有把你當成員工,而是把你當成博士生來看待。我對你的責任就是希望能夠讓你有一技之長,別人不是因為你是泰迪軟體的Erica而付你錢,而是因為你是Erica而付你錢。這一天到來,就等於你博士班畢業。」

其實Erica的「點數」在2019年已經集滿,達到畢業的標準,算是在泰迪軟體多幫忙2年。當年Teddy博士班唸了N年,雖然捨不得但終究還是要離開實驗室。「畢業」,是另一個成長的開始,不是結束。


泰迪軟體又回到只有Teddy的公司,反映公司的名稱,可能是當初取名子就已經決定好的宿命XD。

***

感恩

2021-12-29 22.53.51

▲難得三喵同床

 

明年Teddy預計將ezKanban部署成微服務架構,在微服務架構上再多花點時間深入研究,同時準備這方面的新課程。另外,最近Teddy找到一個在DDD中簡單落實Living Documentation的方法,明年也會花點時間在這上面。

最後感謝2021年直接或間接金援泰迪軟體的所有鄉民與親朋好友。沒有金錢的幫助,泰迪軟體無法經營下去,當然也就沒有現在的Teddy。

對於一個不拉幫結派、不搞花俏行銷、不造神,相信把自己的專長持續做到最好就有一線生機的笨蛋,還可以在這片土地上生存下去,而且活得快樂、活的有尊嚴。除了感恩,還是感恩。

***

友藏內心獨白:想不到更好的,重複使用了2015、2019年的結語。


愛上Mob Programming之突破瓶頸篇

$
0
0

Jan. 11 03:22~04:55

截圖 2022-02-11 上午3.45.16

▲圖1:Pitest產生的涵蓋率報表

 

前言

今年初開始幫ezKanban的use case tests(使用案例的測試案例)改用Given-When-Then的格式讓它更接近Living Documentation,如圖2。改完之後發現use case tests與Aggregate Root測試案例(單元測試)有許多重疊的現象。因為ezKanban的所有Aggregate Root都包含了合約(Contracts),在程式執行期間會自動驗證程式正確性,因此Teddy就在想「是不是可以透過Specification by Example方式所撰寫的驗收案例,加上幫Aggregate撰寫合約,來省略entity layer的單元測試」。

截圖 2022-02-11 上午3.44.38

▲圖2:加上Given-When-Then的測試案例

 

如果這個想法成立,就可以在確保程式正確性的前提之下少寫很多單元測試 。問題是怎麼知道拿掉entity layer的單元測試只要有Aggregate Root的合約依然可以確保程式正確性呢?軟體測試中有一種叫做Mutation testing(變異測試)的方法可以回答這個問題。

 

***

驗證關卡1

有兩位非常優秀的ezKanban的成員負責驗證這個想法,這個題目並成為其中一位的碩士論文。他們找到Pitest這個Mutation testing工具,但是使用在ezKanban的時候遇到問題一直無法解決。因為這是個最新冒出來的題目,所以還沒有機會在ezKanban團隊的mobbing活動中一起處理,這兩位成員是利用mobbing以外的時間去嘗試解決。

昨天和ezKanban團隊mobbing,上午把這幾周以來一直在處裡的持續整合工作告一段落。盤點一下手邊的工作,團隊決定一起看看Pitest的問題。團隊手邊有一個測試用的小專案,可以正常執行Pitest,但是當團隊在ezKanban的專案中執行Pitest卻會出現如圖3的不明錯誤訊息。

 

截圖 2022-02-11 上午4.07.46

▲圖3:Pitest錯誤不明訊息

 

由於團隊是透過maven去執行Pitest,因此大家懷疑是不是maven專案的pom.xml檔案設定有問題。於是團隊試著修改pom.xml檔案但問題還是沒解決。後來Teddy想到,既然有一個可以正常執行的專案,那乾脆把ezKanban的程式碼複製到這個專案中看看能不能執行,這樣子就可以確定到底是ezKanban程式碼導致Pitest執行失敗,還是因為maven設定的問題。

將ezKanban程式碼複製過去之後可以正常執行Pitest,於是團隊試著比較兩個專案的pom.xml的差異。大家看來看去也看不出來到底是哪裡有問題,索性將兩個檔案文字比對,如圖四)。

 

截圖 2022-02-11 上午4.19.42

▲圖4:比對兩個專案的pom.xml差異

 

發現原來ezKanban使用JUnit 5的artifactId是junit-jupiter-api,如下:

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
</dependency>

 

而Pitest只支援JUnit 4,但有鄉民幫它加工之後讓它可以在Junit 5執行,但是此時artifactId要改成junit-jupiter。

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>

 

改用正確的依賴之後,Pitest就可以產生如圖1的報表。

***

 

驗證關卡2

ezKanban系統由若干的maven專案所構成,一開始團隊使用DDDCore這個相依性最簡單,只包含Clean Architecture的Entities Layer與Use Cases Layer,沒有使用到SpringBoot與資料庫的專案來測試Pitest。測試成功之後改用Account專案,這個專案除了包含Clean Architecture中的完整四個Layers,除了使用到SpringBoot與資料庫,還用到Java 17最新的預覽功能,例如Pattern Matching for Switch。

執行Pitest之後出現圖5所示的錯誤訊息,。

截圖 2022-02-11 上午4.36.52

▲圖5:Pitest無法辨認byte code格式

 

很簡單啊,就是執行Pitest的時候加上—enable-preview參數就好了啊。問題是,要加在哪裡?試了幾種方式,後來有團隊成員找到正確的格式,如圖6所示。

 

截圖 2022-02-11 上午4.41.23

▲圖6:加上—enable-preview參數讓Pittest支援Java 17預覽功能

 

***

打破個人的瓶頸

瓶頸就是系統中生產力最弱的環節,每一個人都有自己擅長的地方,也有自己鬼遮眼的時候。軟體開發是一個動態的系統,採用傳統的單人開發模式(solo programming),每位開發人員同時各自開工,看似生產效率很高,但事實上很可能這些各自執行的thread經常處於block(卡住)狀態。

Pair programming可以稍微改善這種情況,但兩個人一組還是比不上全部的人一組。你可能會說:「全部的人一組還是會有盲點啊,可能這個鬼很厲害,把全部的人的眼睛都遮住了。」沒錯,可能會這樣。但從Boundary(邊界)的角度來看,整個團隊一起開發已經是這個團隊能力範圍內最大的「邊界」,也就是他們已經同時間盡其所能的一起合作解決問題。如果這個「鬼」那麼厲害能夠遮住全部人的眼睛,你派一個人或是兩個人去對付這個鬼,更加無法打敗它。

 

***

 

友藏內心獨白:團隊一起處裡有價值的工作。

領域模型 VS 資料模型

$
0
0

March 18 15:36~17:02

 

截圖 2022-03-18 下午4.49.39

▲看板桌遊

 

問題

昨天在北科上軟體架構請學生練習Event Storming,有學生問Teddy:「域驅動設計強調要建立域模型,但我不太清楚領域模型 (domain model) 與資料模型 (data model) 有什麼不一樣?領域模型裡面物件的屬性,和資料模型的資料欄位不是很類似嗎?

領域驅動設計強調透過開發人員與領域專家共同合作建立領域模型,而不要採用傳統資料驅動或是使用者介面驅動的方式來開發軟體。道理很簡單,但領域模型與資料模型到底有何不同,許多人並無法分辨。

今天來談這個問題。

***

資料驅動

請看圖1和圖2畫面,你會覺得這是兩個不同的軟體,還是同一個軟體?

 

截圖 2022-03-18 下午4.00.07

▲圖1:ezKanban畫面

 

截圖 2022-03-18 下午4.00.17

▲圖2:看板桌遊畫面

 

如果從「資料驅動」的角度來看,看了這兩個圖,大部分的人會覺得這是兩個不同的系統。以圖2為例,畫面上有以下不同種類的卡片:

  • 事件卡片:Day 9那張卡片代表event card,它的背面也有資料。你可能在資料庫中設計一個event_card table來儲存事件卡片的資料。
  • 標準卡片:S開頭的卡片,如果設計一個standard_card table來儲存它的資料,則該table欄位可能包含analysis_total_work, analysis_done_work, dev_total_work, dev_done_work, test_total_work, test_done_work, day_deployed, day_ready, lead_time, subscribers等欄位。
  • 固定交期卡片:F1與F2卡片,這兩張卡片的內容與標準卡片很像,感覺可以把資料放在standard_card table,只要多增加一個card_type欄位來區分卡片種類是標準卡片還是固定交期卡片即可。
  • 技術卡片:和前兩種卡片也很像,可以把資料放在standard_card table

看板遊戲還有骰子、Ready、Analysis、Dev、Test、Ready to Deploy、Deployed等固定的工作階段,也要設計相對應的資料庫表格來儲存這些資料。

***

至於圖1的ezKanban,卡片內容記錄的資料和看板桌遊不一樣,所以可能需要一個card table來紀錄資料,裡有可能有cardId, workflowId, laneId, description, deadline, note, estimate, assignees 等欄位。很明顯地,兩個系統的資料模型並不一樣。

***

領域驅動

如果從領域驅動的角度來看,ezKanban與看板桌遊廣義來看都屬於「看板」這個問題領域,所以它們的商業邏輯與行為很可能絕大部分是相同的。例如,從看板系統的三個核心原則來看:

  • 視覺化:視覺化團隊的工作流程與工作項目
  • 限制WIP:限制工作階段的WIP (Work In Progress)
  • 管理工作流:可以測量Lead Time與Cycle Time,看到被阻礙的工作。

這三個特性,在ezKanban與看板桌遊都成立。從領域驅動的角度來看,兩者可以視為同一個系統,共享相同的領域模型,請參考圖3,只不過前端顯示方式不同。

 

截圖 2022-03-18 下午4.58.31

▲圖3:ezKanban領域模型,看板桌遊也可以共用此領域模型

 

***

看到這裡鄉民們可能會想:「兩個系統的資料模型明明就不一樣啊,硬要說它們是同一個系統,那麼要如何儲存兩者的資料,資料庫要怎麼設計?」

很簡單,如果把看板桌遊視為ezKanban的一部分,那麼看板桌遊的事件卡片、標準卡片、固定交期卡片、技術卡片,就全部都是ezKanban裡面Card這個Aggregate的一種特例。只要將看板桌遊的卡片所需要的資料以JSON的格式存入Card的note欄位即可,如圖4所示。

 

截圖 2022-03-18 下午4.30.04

▲圖4:標準卡片S9所需的資料,用JSON格式表達

 

***

結論

ezKanban因為採用領域驅動設計的方式開發,因此將看板桌遊視為看板系統的一種特例,只有前端顯示桌遊的React程式不同,後端絕大部分的功能都是一樣,不需重複開發。

但如果從資料驅動設計使用者介面驅動設計的角度來看,後端就非常有可能變成兩個完全不同的系統,產生很多重複的工作。

***

友藏內心獨白:不管是單體還是微服務,有複雜邏輯的系統還是採用領域驅動設計比較好。

物件聚合與類別繼承的取捨

$
0
0

March 21 15:16~16:14

截圖 2022-03-21 下午3.23.02

 

不是說好要少用繼承嗎?

昨天上「Design Patterns這樣學就會了–入門實作班」,講完Template Method設計模式之後Teddy問學員:「GoF不是說Favor object composition over class inheritance,但Template Method卻使用class inheritance,為什麼?你們能不能用object composition 達到Template Method的效果?

 

圖1是Teddy上課時設計的Template Method範例,鄉民們一起想想看,如何用object composition來取代Template Method。

截圖 2022-03-21 下午3.24.11

▲圖1:Template Method類別圖

***

不使用繼承的設計

圖2是使用object composition取代class inheritance的設計,原本Template Method所呼叫的primitive operations或是hook operations改呼叫實作Operation的具體類別,請參考圖3程式碼。

 

截圖 2022-03-21 下午3.53.02

▲圖2:用object composition取代class inheritance (套用Command設計模式取代Template Method)

 

截圖 2022-03-21 下午3.51.19

▲圖3:圖2中的ConfigParser程式示意範例

 

***

哪種設計比較好?

如果只從Favor object composition over class inheritance的觀點來思考,圖2套用Command設計模式的設計比較好。但是,請數一下這兩個設計所需要的類別/介面數量:

  • Template Method:如圖1所示需要3個類別:
  • Command:參考圖3,ConfigParser與Operation這兩個是固定的,要分別支援從檔案與資料庫讀取設定資料,所以需要兩種DataSource實作,因此最少需要 1 + 1 + 2 + 4 = 8個類別/介面。

 

從Kent Beck的建單設計(Simple Design)原則來看:

  1. Passes the Tests
  2. Reveals Intention
  3. No Duplication
  4. Fewest Elements

第4條:最少元素,達到相同的功能,在這個例子裏面Template Method(class inheritance)用了3個「元素」,而Command(object composition)則用了至少8個「元素」,因此在這裡用Template Method的設計應該是比較簡單的設計。

***

友藏內心獨白:繼承不是不能用。

事件溯源(1):Event Sourcing的好處

$
0
0

June 28 06:30~09:30

截圖 2022-06-28 上午7.00.28

▲採用Event Sourcing的系統狀態由重播(重新套用)事件所計算而來

 

前言

今年下半年Teddy準備開新課程—【事件溯源與命令查詢責任分離架構入門實作班】,開課之前先把教材內容整理成文章。這系列文章分成Part 1與Part 2,先介紹Event Sourcing在介紹CQRS。

 

***

 

兩種常見儲存狀態的方式

Event Sourcing,翻譯成「事件溯源」或「事件來源」,是一種儲存狀態的方式。在Event Sourcing流行之前,大部分的開發人員儲存系統狀態的方式稱為State Sourcing(狀態來源)或Domain Sourcing(領域來源)。如圖1所示,State Sourcing採用將系統目前狀態儲存至資料庫中。這種儲存狀態的重點是資料庫中只儲存目前狀態,系統的狀態是直接從資料庫中取得,至於所使用的資料庫是關聯式資料庫或是NoSQL資料庫都可以。

在State Sourcing系統中,狀態改變會直接覆寫物件現有的狀態,例如圖1中Teddy的email如果改成ted@gmail.com則在資料庫中id=001的這一筆資料,其email欄位的值就被改成ted@gmail.com,舊有的值則被覆蓋掉。

 

截圖 2022-06-28 上午7.09.04

▲圖1:以State Sourcing保存系統狀態

 

***

 

如圖2所示,Event Sourcing在資料庫中保存的不是系統的目前狀態,而是儲存曾經造成系統狀態改變的所有事件。至於系統的目前狀態,則是透過把這些事件從頭到尾重新執行過一次(稱為replay events)所計算出來。儲存事件的資料庫一般稱為Event Store,它可以是關聯式資料庫,例如message-db (https://github.com/message-db/message-db)、NoSQL資料庫,或是專門為Event Sourcing所設計的特殊用途資料庫,例如EventStoreDB (https://eventstore.com)。

在Event Sourcing系統中,基本上採取append only的方式來儲存事件。事件只可寫入Event Store,不可刪除或修改。例如圖2中Teddy的email如果改成ted@gmail.com,則在資料庫中代表Teddy帳戶的event stream會被寫入一筆新的事件:EmailChanged { id=001, email=ted@gmail.com}

截圖 2022-06-28 上午7.07.00

▲圖2:以Event Sourcing保存系統狀態

 

***

稽核

Event Sourcing並不是新的技術,像是銀行的存款帳戶儲存客戶存款金額的方式就是採用Event Sourcing,存摺上面一筆、一筆的交易紀錄(transaction log)就是系統所儲存的事件。.

由於所有針對系統所造成的狀態改變皆以事件的形式儲存起來,因此可以達到稽核的效果。以銀行存款為例,如果銀行採用State Sourcing的方式儲存你的存款餘額,你認為自己在銀行存款餘額還有一百萬,但是銀行的資料紀錄你只剩下一百塊,怎麼辦,以誰的紀錄為準?為了稽核,銀行必須儲存每筆交易紀錄,既使是錯誤的交易紀錄也不能直接刪除,而是要用另外一筆紀錄去沖銷。例如,銀行帳務系統錯誤,不小心轉了五萬到你的戶頭。銀行不能直接從你的戶頭扣掉五萬然後刪除這筆交易紀錄,而是要新增一筆負五萬的交易紀錄來抵銷原本錯誤的交易。

 

***

 

狀態同步

既然Event Sourcing不是新技術,在特定領域中也應用了很長一段時間,為什麼近幾年來這個名詞變得越來越流行?它流行的原因和DDD(領域驅動設計)流行的原因相同,主要受惠於微服務架構的熱潮。

在DDD中,Aggregate狀態改變會產生領域事件(Domain Event),透過領域事件可以在不同Aggregate之間達到狀態最終一致性,這個特性正好可以應用在分散式或微服務架構中作為狀態同步之用。

如圖3所示,如果DDD的Aggregate採用State Sourcing,在儲存物件狀態的時候,除了原本物件的目前狀態以外,還需要儲存領域事件,這兩個動作必須要在同一個交易(transaction)中一起完成,系統狀態才不會錯誤。在微服務物架構中Transactional Outbox設計模式就是用來解決這個問題。

 

截圖 2022-06-28 上午8.21.37

▲圖3:State Sourcing保存系統狀態與領域事件必須在同一個交易中

 

如圖4所示,採用Event Sourcing的系統在儲存領域事件的同時就等於儲存系統狀態,也就避免圖3的問題。

 

截圖 2022-06-28 上午8.30.28

▲圖4:Event Sourcing保存領域事件也就同時保存系統狀態

 

在這裡有一點要注意,不管是採用Transactional Outbox或是Event Sourcing,在這種情況下Event Store同時也扮演簡易Message Bus或Message Broker的功能。也就是Event Store需要支援客戶端去資料庫中讀取領域事件,然後再將領域事件轉發給其他「下游」的Event Handler或微服務,如圖五。

 

截圖 2022-06-28 上午8.53.46

▲圖5:Event Store也是Event Bus/Event Broker

***

 

偏好寫入

Event Sourcing還有寫入快速、簡單、方便的好處。在DDD的情境下採用Event Sourcing,每一個Aggregate的instance在Event Store中會有一個專屬的event stream用來儲存它的領域事件 ,這個event stream通常以Aggregate Type-Aggregate ID的格式來命名,例如一個Account Aggregate instance,它的id等於3104ca15-df4a-4878-9342-b6d6d650b4cc,那麼在Event Store中就會新增一個Account-3104ca15-df4a-4878-9342-b6d6d650b4cc的event stream,用來儲存該Account instance的領域事件,如圖6所示。

 

截圖 2022-06-28 上午9.10.21

▲圖6:EventStoreDB的Stream Browser畫面

 

相較於使用關聯式資料庫需要將物件結構轉成關聯式表格(Object Relational Mapping;ORM),Event Sourcing只需儲存領域事件,省去ORM繁瑣的設定。另外,由於寫入資料一次只針對一個Aggregate,所以也不會有採用關聯式資料庫在寫入時可能需要鎖定多個表格的現象,因此「理論上」Event Sourcing的寫入效能會比較高。

 

***

下集預告

關於Event Sourcing的基本觀念先介紹到這裡,下集就要開始寫程式,以ezKanban的Tag Aggregate為例子,說明撰寫Event Sourced Aggregate以及其相對應Repository的步驟。

 

***

友藏內心獨白:終於又開工了。

事件溯源(2):實作Event Sourced Aggregate

$
0
0

June 28 10:19~12:19

截圖 2022-06-28 上午10.26.08

▲圖1:ezKanban簡化版的domain model

 

練習題目:Tag Aggregate

在領域驅動設計(Domain-Driven Design)中Aggregate(聚合)是一群物件的集合,它們的狀態改變必須在同一個交易中完成。也就是說Aggregate形成交易邊界,也是DDD中儲存狀態的最小物件單位。Aggregate的狀態交由Repository設計模式來儲存與讀取,一個Aggregate type對應到一個Repository

圖1是ezKanban系統core domain的領域模型,一共有四個aggregate,今天要用Event Sourcing的方式實作最簡單的Tag aggregate。

***

CreateTagUseCase Test

Teddy上【領域驅動設計與簡潔架構入門實作班】的時候採用TDD串起DDD、Event Storming與Clean Architecture的開發流程,要撰寫Tag aggregate依照慣例先幫它寫驗收測試案例,從CreateTagUseCaseTest開始。

圖2為CreateTagUseCase的驗收測試案例,Tag的屬性有tag id, board id, name, color這四個欄位。第37行執行完畢後一個新增的Tag會透過TagRepository被加入資料庫中。第29行透過CreateTagUseCase的建構函數將TagRepository注入給CreateTagUseCase。此時尚不需要決定TagRepository的實作細節,因此先用一個InMemoryTagRepository來「欺騙」CreateTagUseCase。

測試案例剛寫好的時候會有語法錯誤無法編譯,這是因為production code還沒寫,接下來撰寫CreateTagUseCase來消除語法錯誤。

 

截圖 2022-06-28 上午10.48.15

▲圖2:CreateTagUseCase驗收測試案例

***

CreateTagUseCase程式碼如圖3所示,很簡單就是新增一個Tag然後把它存入repository。同樣的,此時由於Tag尚未撰寫因此會有語法錯誤無法編譯程式。

 

截圖 2022-06-28 上午10.59.06

▲圖3:CreateTagUseCase

***

接著撰寫Tag單元測試,如圖4。

 

截圖 2022-06-28 上午11.06.04

▲圖4:Tag單元測試

 

最後撰寫Tag,圖圖5所示,它繼承ezKanban內建的AggregateRoot類別,第22行程式產生TagCreated領域事件代表Tag狀態改變。如果不管Event Sourcing,CreateTagUseCase使用案例就寫好了。但是因為Tag想要套用Event Sourcing,所以程式撰寫方式要加以修正。

截圖 2022-06-28 上午11.34.09

▲圖5:沒有支援Event Sourcing的Tag aggregate

 

***

Event Sourced Tag

要讓Aggregate支援Event Sourcing,只要把握以下原則即可:

首先,Aggregate的public Command(改變系統狀態的操作)只負責產生領域事件,然後呼叫定義在AggregateRoot身上的apply方法去套用這個領域事件,如圖6所示。

 

截圖 2022-06-28 上午11.16.11

▲圖6:支援Event Sourcing的Tag aggregate建構函數

 

其次,如圖7所示AggregateRoot的apply方法是一個Template Method,它依次呼叫ensureInvairant、when、ensureInvairant、addDomainEvent。在此先忽略ensureInvairant,接下來的when method是一個Template Method設計模式裡面的primitive operation, 子類別必須實作when用來撰寫事件處理程式(event handler)。addDomainEvnet則是將領域事件先保存在AggregateRoot身上,最後Repository可讀取AggregateRoot的領域事件來儲存狀態。

截圖 2022-06-28 上午11.21.34

▲圖7:AggregateRoot的apply method

 

最後,如圖8所示,Tag實作when方法來處哩TagCreated領域事件。處理方式很簡單,只要把圖5中原本Tag建構函數的初始化程式碼移到when裡面即可。

 

截圖 2022-06-28 上午11.29.12

▲圖8:Tag實作when方法

***

實作rename方法

可以新增Tag之後,接下來看Tag的rename method,如圖9所示,首先判斷傳入的newName和Tag現有名字是否相等,如果是就直接離開(因為狀態沒有改變),若否則apply TagRenamed領域事件。

 

截圖 2022-06-28 上午11.46.10

▲圖9:Tag的rename方法

***

 

刪除Tag

傳統關聯式資料庫的刪除最簡單的做法就是把該筆資料直接從資料庫刪除(delete),但在Event Sourcing系統中基本上只會append事件不會把事件刪除,所以實作刪除的方式就是在AggregateRoot定義markAsDeleted方法,然後讓它的子類別去實作該方法。如圖10所示,Tag的markAsDeleted方法apply TagDeleted領域事件。Tag最後的when方法處哩TagCreated、TagRenamed、TagDeleted三個領域事件,如圖11所示。

 

截圖 2022-06-28 上午11.55.57

▲圖10:Tag的rename方法

 

截圖 2022-06-28 上午11.59.36

▲圖11:Tag的when方法,處理三個領域事件

 

***

從領域事件回復狀態

將Tag改成上述寫法之後,要透過領域事件回復狀態就非常簡單。請參考圖12單元測試案例,第69與71行分別新增TagCreated與TagRenamed領域事件,模擬從資料庫中讀取Tag的領域事件。第74行新增Tag,將剛剛產生的兩個領域事件傳給Tag建構函數。最後驗證tag的狀態是否正確。

截圖 2022-06-28 下午12.05.59

▲圖12:Tag的when方法,處理三個領域事件

 

如圖13所示,Tag建構函數接受一個,List<DomainEvent>,它直接呼叫super,也就是圖14。

截圖 2022-06-28 下午12.11.37

▲圖13:Tag建構函數

 

如圖14所示,AggregateRoot建構函數收到List<DomainEvent>之後,第38行跑一個for each逐一將領域事件傳給apply方法。也就是說「將事件重播(replay)以計算出目前狀態」

截圖 2022-06-28 下午12.11.55

▲圖14:AggregateRoot建構函數

***

下集預告

撰寫好支援事件溯源的Tag Aggregate之後,下集介紹如何將狀態儲存到EventStoreDB (https://eventstore.com) 資料庫中。

***

友藏內心獨白:是不是很簡單。

事件溯源(3):將Aggregate儲存至EventStoreDB

$
0
0

June 29 05:39~09:08

截圖 2022-06-29 上午9.06.33

▲ezKanban團隊討論Repository設計的記錄

 

前言

今天要將上一集寫好的Tag Aggregate儲存到EventStoreDB (https://eventstore.com) 這個專門為Event Sourcing所設計的特殊用途開源資料庫。

***

 

準備環境

首先安裝EventStoreDB,官方已經製作好現成的Docker影像檔,無須安裝執行以下指令便可直接使用:

 

docker run --name esdb-node -it -p 2113:2113 -p 1113:1113 eventstore/eventstore:latest --insecure --run-projections=All --enable-external-tcp --enable-atom-pub-over-http --start-standard-projections

 

如圖1所示,打開瀏覽器輸入以下網址連上EventStoreDB管理畫面: http://localhost:2113/web/index.html#/dashboard

 

截圖 2022-06-29 上午5.53.17

▲圖1:EventStoreDB管理畫面

 

切換到Stream Browser頁面,如圖2所示。之後儲存至EventStoreDB的Aggregate將會出現在這個頁面。

截圖 2022-06-29 上午5.57.05

▲圖2:EventStoreDB的Stream Browser頁面

 

接著安裝EventStoreDB的客戶端驅動程式,Teddy採用Maven管理專案依賴,在pom.xml檔案加入以下依賴:

<dependency>
    < groupId>com.eventstore</groupId>
    < artifactId>db-client-java</artifactId>
    <version>2.0.0</version>
</dependency>

目前EventStoreDB最新版的Java驅動程式是3.0.0,Teddy範例程式使用的是2.0.0版本。

***

 

Repository設計與實作

在領域驅動設計中(Domain-Driven Design;DDD)透過Repository設計模式儲存與讀取Aggregate,圖3是Teddy設計的AbstractRepository介面,所有Concrete Repository皆會實作此介面。

 

截圖 2022-06-29 上午6.24.26

▲圖3:AbstractRepository介面

 

圖4為TagRepository介面,它實作AbstractRepository。

截圖 2022-06-29 上午6.24.42

▲圖4:TagRepository介面

 

接下來討論TagRepository的實作。雖然本系列的主題是Event Sourcing,但實務上很多開發人員可能因為公司規定或是個人偏好,還是習慣將資料儲存在關聯式資料庫。因此Teddy希望TagRepository的實作能夠同時支援Event Sourcing以及State Sourcing加上Transactional Outbox

最後的設計圖如圖5,TagRepository有兩個實作,分別是TagEventSourcingRepository與TagOutboxRepository。前者支援Event Sourcing,後者支援State Sourcing加Transactional Outbox。今天先討論TagEventSourcingRepository的實作,下一集再來看TagOutboxRepository。

 

截圖 2022-06-29 上午6.35.32

▲圖5:支援Event Sourcing與State Sourcing加Transactional Outbox的Repository架構設計圖

***

 

TagEventSourcingRepository實作

 

TagEventSourcingRepository程式如圖6所示,基本上它把要做的事情都委託給GenericEventSourcingRepository來完成。

截圖 2022-06-29 上午7.01.36

▲圖6:TagEventSourcingRepository程式碼

 

圖7為GenericEventSourcingRepository實作,首先看到26行的findById,它透過eventSourcingStore依據Aggregate stream name從資料庫中讀出所有的領域事件(第28~29行)。由於Teddy套用Clean Architecture,所以寫入與讀取自事件溯源資料庫中的資料有一個統一的介面AggregateRootData

第33~34行將AggregateRootData身上的DomainEventData轉成DomainEvent,然後第36行使用Java Reflection技術產生所指定的Aggregate instance,並將剛剛從資料庫中所讀取的領域事件傳給它。Aggregate的建構函數收到領域事件之後會重播(replay)這些事件以便計算出最新的狀態。最後回傳Aggregate instance,如此便成功從事件溯源資料庫中讀取Aggregate。

第48行的save方法比較簡單,首先在51行將傳入的Aggregate轉成AggregateData,然後第52行透過eventSourcingStore儲存這個AggregateData。儲存完畢第53行後重設Aggregate版本,然後在54行清除Aggregate身上的領域事件(因為Aggregate的狀態已經儲存到資料庫,所以要清除它身上的領域事件,否則相同Aggregate若再儲存一次會儲存重複的領域事件)。

截圖 2022-06-29 上午7.08.24

▲圖7:GenericEventSourcingRepository程式碼

 

***

 

EsdbStore實作

以上看了老半天鄉民們可能會問:「我還是沒看到領域事件到底如何存到EventStoreDB啊?」因為ezKanban支援不同的事件溯源資料庫,所以採取事件溯源儲存方式的Repository實作是透過EventSourcingStore介面來操作資料庫(請參考圖5)。

圖8中的EsdbStore類別是ezKanban針對EventStoreDB所提供的EventSourcingStore實作,第14行的EventStoreDBClient類別就是EventStoreDB官方提供的EventStoreDB驅動程式,開發人員最終要透過它跟EventStoreDB連線與讀、寫資料。如果鄉民們沒有ezKanban套用Clean Architecture與支援多種事件溯源資料庫的需求,就可以直接在Repository實作類別中使用EventStoreDBClient操作EventStoreDB,這樣子會簡單很多。

接下來看第16行的save方法,它接受AggregateRootData當作輸入參數。這個類別Teddy剛剛介紹過,它是ezKanban為了符合Clean Architecture的跨層原則,設計用來將Aggregate轉成AggregateRootData再傳給資料庫的類別。第21~28行將AggregateRootData身上的DomainEventData轉成EventDataEventData是EventStoreDB驅動程式所設計的資料結構,它是真正存到EventStoreDB的物件。

在操作關聯式資料庫的時候,開發人員要指定Table(資料表),EventStoreDB操作的對象不是Table,而是event stream(事件流,可以想像成它就是一個append only的檔案)。開啟event stream的時候可以指定開啟模式,在EventStoreDB的驅動程式中以AppendToStreamOptions類別表示,如圖8的第31~38行。

EventStoreDB支援樂觀鎖定(optimistic locking),若第32行aggregateRootData.getVersion() 回傳-1代表產生一個新的Aggregate,第34行expectedRevision的參數設定為ExpectedRevision.ANY即可。在此模式底下寫入資料的時候EventStoreDB不會做樂觀鎖定的檢查。如果不是產生新的Aggregate,如37行所示則將expectedRevision的參數設定為new StreamRevision(aggregateRootData.getVersion())。StreamRevision是EventStoreDB驅動程式用來代表event stream中事件版本的物件,在此直接把aggregateRootData.getVersion() 當成參數傳給它即可。設定了expectedRevision之後,寫入資料的時候EventStoreDB就會啟動樂觀鎖定檢查。

最後,第39~41行透過EventStoreDBClient類別的appendToStream方法把資料寫入資料庫中。

截圖 2022-06-29 上午7.37.55

▲圖8:EsdbStore程式碼

 

接下來看圖8第57行的load方法,它從Aggregate instance所屬的event stream中讀取資料(每一個Aggregate instance都有一個專屬的event stream儲存它自己的領域事件)。第63~71行呼叫getResolvedEvents方法(參考圖9)讀取資料,讀出的資料格式是EventStoreDB驅動程式所定義的ResolvedEvent類別。

第76行新增aggregateRootData,然後在77~79行將剛剛從資料庫讀出的ResolvedEvent轉成DomainEventData,最後在第84行回傳aggregateRootData。

 

截圖 2022-06-29 上午8.29.03

▲圖9:EsdbStore類別的getResolvedEvents方法

 

***

 

執行測試案例

完成TagRepository的整個實作之後,改寫上一集<事件溯源(2):實作Event Sourced Aggregate>的測試案例,把原本InMemoryTagRepository換成TagEventSourcingRepository,如圖10所示。

 

截圖 2022-06-29 上午8.43.50

▲圖10:將測試案例中的tagRepository換成TagEventSourcingRepository實作

 

執行完測試案例打開EventStoreDB管理畫面,看到新增一筆event stream,如圖11所示。

截圖 2022-06-29 上午8.53.37

▲圖11:從EventStoreDB管理畫面看到剛剛新增的event stream

 

點開這筆event stream,看到裡面有一筆資料,它的型態的TagEvents$TagCreated事件,資料內容採用JSON格式儲存,如圖12所示。

截圖 2022-06-29 上午8.53.46

▲圖12:用EventStoreDB管理畫面觀看event stream的內容

 

***

下集預告

下一集介紹用State Sourcing加Transactional Outbox方式實作TagRepository。

 

***

友藏內心獨白:這一篇居然寫了這麼久。

事件溯源(4):將Aggregate儲存至Outbox Store

$
0
0

June 30 10:41~12:02;12:56~15:24

截圖 2022-06-30 上午11.35.04

▲圖1:Outbox儲存方式,資料庫中包含State Sourcing與Transactional Outbox所需的資料表

前言

這一集要用傳統State Sourcing方式將Tag Aggregate儲存到PostgreSQL關聯式資料庫中,除了透過ORM工具將Tag Aggregate資料儲存到資料庫表格中,還需要套用Transactional Outbox在儲存Tag的同一個交易中一併將它身上的領域事件儲存到資料庫的領域事件表格中。我們將這種同時在同一個交易中儲存現有狀態以及領域事件的儲存方式簡稱為Outbox。

***

準備環境

如圖1所示,採用Outbox的資料庫需要有一個用來儲存領域事件的預設表格,Teddy使用Message DB這個開源軟體(https://github.com/message-db/message-db)。它已經設計好用來儲存事件的資料庫表格(表格名稱叫messages),詳細使用方法請參考它的官方網站。

圖2為ezKanban使用Message DB的資料庫畫面,其中messages表格由Message DB所建立,其它像是board、 board_content、board_member、card等表格則是ORM自動建立(ezKanban使用JPA來自動產生這些表格)。

截圖 2022-06-30 上午11.53.44

▲圖2:ezKanban採用Outbox的資料庫筆格(部分畫面)

***

TagOutboxRepository實作

上一集<事件溯源(3):將Aggregate儲存至EventStoreDB>已經談過Repository的設計如何同時支援Event Sourcing與Outbox這兩種資料儲存方式,這一集就直接實作TagOutboxRepository類別。程式如圖3所示,它的實作方式和TagEventSourcingRepository類似,差別在於TagEventSourcingRepository將工作委託給GenericEventSourcingRepository,而TagOutboxRepository則是委託給GenericOutboxRepository

 

截圖 2022-06-30 下午12.59.39

▲圖3:TagEventSourcingRepository程式碼

 

圖4為GenericOutboxRepository實作,首先看到11行,它接受兩個泛型參數:AggregateRootOutboxData。前者用來表示該GenericOutboxRepository是給哪一個Concreate Aggregate使用,後者則是該Concreate Aggregate透過Outbox方式儲存到資料庫所需的資料。

 

截圖 2022-06-30 下午1.07.05

▲圖4:GenericOutboxRepository類別

 

圖5為TagData類別,基本上它身上有Tag Aggregate所有需要儲存到資料庫的屬性(第17~25行),加上@Id與@Column這些JAP的annotation。第14行的streanName與第16行的domainEventDatas這兩個屬性是用來保存領域事件的資料,最後會被儲存至messages這個資料庫表格。最後26~28行的version是用來支援樂觀鎖定所使用的屬性。

 

截圖 2022-06-30 下午1.19.46

▲圖5:TagData類別

 

繼續看到圖4第23行的findById,在第25行它透過OutboxStore介面依據Aggregate id從資料庫中直接找出代表該Aggregate的Outbox Data物件。以Tag Aggregate,為例,這個Outbox Data物件的實作就是TagData。接著第27行將這個Outbox Data物件透過OutboxMapper轉成Aggregate並回傳;以Tag為例,將TagData轉成Tag然後回傳Tag給findById的呼叫者。

接下來看到第33行的save方法,首先在第35行將傳入的Aggregate轉成Outbox Data,然後第36行透過OutboxStore介面將Outbox Data儲存至資料庫。儲存完畢後第37行重設Aggregate版本,然後在第38行清除Aggregate身上的領域事件(因為Aggregate的狀態已經儲存到資料庫,所以要清除它身上的領域事件,否則相同Aggregate若再儲存一次會儲存重複的領域事件)。

在這裡有一個重點,就是第36行的store.save()方法。以Tag Data為例,它會先把Tag Data儲存到資料庫中的Tag Table,然後在把它身上的領域事件儲存至資料庫中的messages Table。這兩個儲存動作會被放在同一個交易中執行,以確保Tag的狀態正確。

***

PostgresOutboxStore實作

上面提到的上OutboxStore介面在ezKanban中有一個PostgresOutboxStore類別實作它,圖如6所示。可以看出來PostgresOutboxStore又把工作委託給PostgresOutboxStoreClient,真正和資料庫打交道的程式就寫在它身上。

 

截圖 2022-06-30 下午1.44.49

▲圖6:PostgresOutboxStore類別

 

圖7為PostgresOutboxStoreClient程式碼,它透過OrmStoreClientPostgrresMessageStoreClient分別將資料儲存到ORM表格與messages表格。ezKanban底層採用SpringBoot框架,把交易處理交給SpringBoot管理(第22行與第36行的@Transactional annotation)。

第23行的save方法先呼saveAndUpdateVersion方法儲存ORM的資料,接著第25行呼叫saveDomainEventsWithoutVersion儲存領域事件。

第36行的delete方法,先呼叫deleteById刪除ORM表格中的資料,然後再呼叫saveDomainEventsWithoutVersion儲存領域事件。

截圖 2022-06-30 下午1.54.33

▲圖7:PostgresOutboxStoreClient類別

 

最後看到OrmStoreClientPostgrresMessageStoreClient實作,如圖8與圖9。前者繼承SpringBoot的CrudRepository,程式碼很簡單Teddy就不多做說明。

 

截圖 2022-06-30 下午2.12.52

▲圖8:OrmStoreClinet類別

 

PostgresMessageStoreClient是ezKanban幫用Message DB所撰寫Java客戶端驅動程式,Message DB其實只有設計messages Table scheam以及撰寫了幾隻PostgreSQL資料庫的functions讓客戶端程式可以用來寫入與讀取領域事件,但它並沒有提供Java的驅動程式讓Java客戶端可以直接讀寫資料庫。圖9第28行的writeMessage方法就是ezKanban幫它所撰寫的驅動程式,只要直接呼叫這個方法就可以把領域事件寫入Message DB

截圖 2022-06-30 下午2.19.01

▲圖9:PostgresMessageStoreClient類別

 

***

執行測試案例

實作完成TagOutboxRepository之後,改寫上一集<事件溯源(3):將Aggregate儲存至EventStoreDB>的測試案例,將TagRepository注入TagOutboxRepository,如圖10所示。

 

截圖 2022-06-30 下午2.57.36

▲圖10:將測試案例中的tagRepository換成TagOutboxRepository實作

 

 

執行完測試案例打開PostgreSQL管理畫面,看到tag Table與messages Table分別新增一筆資料,如圖11所示。

截圖 2022-06-30 下午2.56.24

▲圖11:從PostgreSQL管理畫面看到新增的資料

 

***

下集預告

Event Sourcing與Outbox這兩種儲存方式都搞定了,但還有一個小細節沒有說明,就是在資料庫中如何儲存「領域事件型別(Event Type)」,下一集討論這個問題。

***

友藏內心獨白:很多細節要處理。


事件溯源(5):如何儲存事件型別

$
0
0

June 30 18:41~19:43

截圖 2022-06-30 下午7.06.54

▲圖1:每一筆領域事件都需要儲存它的型別(Type)資料

 

前言

無論是Event Sourcing或是Outbox,領域事件必須被「序列化(serialize)」之後方可儲存到資料庫,從資料庫讀出則是經過「反序列化(deserialize)」之後變成領域事件物件。這個序列化、反序列化的過程,必須知道領域物件的型別才可以完成。因此,領域物件儲存到Event Store必須記錄事件型別,如圖1所示。今天要討論的問題是:「在Event Store中要如何記錄領域物件型別?」

***

紀錄Package Name + Class Name

這個問題乍看之下好像很簡單,啊不就儲存Domain Event的完整型別就好了?例如儲存TagEvents.TagCreated這個領域事件,只要呼叫:

TagEvents.TagCreated.class.getCanonicalName()

把下列回傳值儲存到資料庫的event type欄位就好了。

ntut.csie.sslab.ezkanban.kanban.tag.entity.TagEvents.TagCreated

反序列化的時候就用 class.forName() 就可以產生領域事件物件。

如果你永遠都不會修改領域事件或是package名稱,也不會把領域事件移動到不同的package,那麼直接儲存領域事件的完整型別是沒問題的。但是,只要將領域事件改名或是移動到不同的package,那麼讀取儲存在Event Store的舊領域事件就會發生 class not found例外。

看到這裡你可能會想:「我去更新資料庫中event type欄位的資料不就好了?」但是Event Sourcing系統有一個特性,就是基本上開發人員不應該去修改已經存在的領域事件,一般而言不建議用這種方式。

 

***

型別對照(Type Mapping)

一般的作法會建議採用型別對照的方式,序列化的時候針對每一種領域事件儲存一個唯一且固定不變的字串到資料庫中,反序列化則是依據這個固定的字串去「查表」,找出相對應的領域事件型別。

實作方法很簡單,首先宣告一個DomainEventTypeMapper介面,如圖2。

 

截圖 2022-06-30 下午7.16.02

▲圖2:DomainEventTypeMapper介面

 

接下來針對每一個Aggregate的所有領域事件,寫一個DomainEventTypeMapper。圖3是給Tag使用的DomainEventTypeMapper,針對TageCreated、TagRenamed、TagColorChanged、TagDeleted這四個領域物件,宣告四個唯一且不變的字串做為寫入到Event Store的事件型別(第55~58行)。

第60行~66行設定這四個領域物件型別字串分別代表哪一個真正的Java領域物件類別,也就是建立型別對照所需要的表格。

截圖 2022-06-30 下午7.20.51

▲圖3:Tga的DomainEventTypeMapper

 

ezKanban在序列化、反序列化的過程會呼叫圖4中的DomainEventMapper類別,第28行toMappintType()方法將領域事件轉成固定的型別字串,第38行則是在反序列化的時候透過領域物件型別字串找到真正的領域物件。

截圖 2022-06-30 下午7.35.07

▲圖4:DomainEventMapper類別

***

下集預告

Event Sourcing對於寫入操作非常方便,不需要做OR-Mapping只需寫入領域事件。但是,要查詢資料的時候怎麼辦?例如,TagRepository只可以依據tagId找到單一個Tag,如果要找出某一個Board裡面的全部Tag,要怎麼辦?下一集討論這個問題。

***

友藏內心獨白:這一集簡單很多。

事件溯源(6):透過Projection查詢Event Store

$
0
0

June 30 22:15~23:38

截圖 2022-06-30 下午10.28.48

▲圖1:EventStoreDB的Projections畫面

 

前言

在領域驅動設計中套用Event Sourcing,一個Aggregate instance的資料透過Repository儲存至Event Store的event stream,讀取的時候也是以單一個Aggregate instance為基本單位。但這樣顯然不足以應付大多數系統需要查詢資料的需求,在進一步套用CQRS解決查詢問題之前,這一集先介紹如何將原本主要用來負責寫入的Event Store也拿來當作查詢資料庫使用。

***

Projection (投影)

接下來Teddy將以EventStoreDB為例,說明如何使用Event Store來查詢資料。之前Teddy提到一般使用Aggregate-ID當作event stream的名稱把Aggregate instance的領域事件存入EventStoreDB。例如一個Tag Aggregate instance,它的id等於4cc11cc7-707b-476a-801c-1b6a22a69169,那麼它的領域事件將儲存在EventStoreDB裡面名稱為Tag-4cc11cc7-707b-476a-801c-1b6a22a69169的event stream。

EventStoreDB中,除了這種代表Aggregate instance的event stream,系統還有一些特殊的event stream:

  • $all:所有系統所產生的事件都可以在$all stream找到。
  • System projections:系統依據某些內建特定條件自動「投影」產生的stream,比較常用的有:
    • By Category($ce-):Category就是Aggregate Type,EventStoreDB會將不同Aggregate Tyee的事件投影至 $ce-[Aggregate] stream中。例如,可以從$ce-Tag stream讀到所有Tag instance的事件。
    • By Event Type($et-):將每一種event type投影成一個event stream,例如可以從 $et-TagEvents$TagCreated stream讀到所有的TagEvents$TagCreated領域事件。

 

***

使用Projection查詢資料

以下用查詢某一個Board有多少個Tag當成範例說明如何使用projection來查詢資料。

在ezKanban中Tag與Board靠著Tag身上的board id維持單向依賴,Board並不知道它身上一共有多少個Tag,所以無法從BoardRepository獲得Board身上有多少個Tag。而基本的TagRepository也只能依據tag id找到某一個Tag,無法找出屬於某一個Board的全部Tag。

參考圖2,要找出某個Board有多少個Tag,只要:

  1. $et-TagEvents$TagCreated stream讀取所有的事件,然後依據事件身上的board id當作過濾條件,就可以找到一個List<TagCreated>用來代表某個Board身上所有 Tag 的id。
  2. 跑一個for each迴圈,依據步驟1找到的List<TagCreated>呼叫TagRepository就可以找出所有這個Board裡面的所有Tag。

 

截圖 2022-06-30 下午11.05.32

▲圖2:getTagsByBoardId程式碼

 

***

 

EventStoreDB Projection的優缺點

EventStoreDB的Projection是一個很強大的功能,它讓原本主要用來支援Write Model的資料庫,也可以同時做為Read Model資料庫。當然這種View Model本質上還是保有Event Sourcing的特性,也就是說要得到領域物件的「目前狀態」還是需要讀出該Read Model裡面所有的領域事件並replay它們。速度可能沒有像是採用CQRS之後,用NoSQL資料庫當作Read Model只要下一個查詢條件就可以直接把整個Read Model讀到記憶體裡面那麼快。

但是,所謂「速度比較慢(或很慢)」也許沒有人類想像中的那麼慢,還是要依據應用程式的實際狀況來判斷,在很多情況之下這種速度已經可以被接受。畢竟直接使用EventStoreDB的Projection可以很方便的產生Read Model,而套用CQRS還需要另外撰寫Projector用以在讀取端資料庫投影出Read Model,這可是一個不小的工作負擔。

另外還有一點要注意,EventStoreDB是採用非同步的方式去投影出這些Projection,也就是說這些Projection與原始寫入的領域事件之間的狀態同步是最終一致性。因此你可能會發現,為什麼有時候領域事件已經寫入資料庫中,但在是Projection裡面還讀不到相關領域事件。

***

下集預告

介紹完Event Sourcing的基本寫入以及讀取操作之後,下一集談在多人同時讀取與寫入資料的情況下,如何透過樂觀鎖定來避免資料衝突。

***

友藏內心獨白:不能下Select查詢資料一開始會不太習慣。

事件溯源(7):樂觀鎖

$
0
0

June 30 22:15~24:00;July 01 00:00~00:59

截圖 2022-07-01 上午12.55.25

▲圖1:不同Aggregate就不用鎖了

 

前言

假設有兩個使用者同時拿到同一個Tag並且將它改名然後儲存,系統要如何避免資料衝突?這是一個並行控制(Concurrency Control)的問題。在Event Sourcing系統中,一般採用樂觀鎖(Optimistic Locking)或稱為樂觀並行控制(Optimistic Concurrency Control)來解決這個問題。

今天介紹在領域驅動設計(Domain-Driven Design;DDD)與Event Sourcing的情境下,如何實作樂觀鎖定。

 

***

樂觀鎖

樂觀鎖的概念很簡單,以上述兩個使用者拿到同一個Tag並將其改名為例,首先Tag在資料庫中保存一個版本號碼,從資料庫讀出的Tag身上帶著這個版本號碼,然後在儲存的時候比對資料庫中的版本號與此時所存入的Tag版本號是否相同。若相同,則表示當初Tag從資料庫讀出之後,沒有其他人修改該Tag,因此可以直接寫入,寫入之後資料庫中該Tag版本號會被加1代表資料被更新過。如果寫入時Tag的版本號與資料庫中的版本號不同,則表示該筆Tag讀出後有人異動過它的資料,因此寫入失敗(不能用舊資料覆蓋掉比較新的資料),丟出樂觀鎖失敗的例外,使用者必須要重新載入新的資料,改修後再次儲存。

***

實作Aggregate與Repository以支援樂觀鎖

請參考圖2,ezKanban的AggregateRoot類別身上的version屬性(第16行)就是用來支援樂觀鎖,它的初始值為-1,表示尚未被寫入到資料庫中。

 

截圖 2022-07-01 上午12.07.34

▲圖2:AggregateRoot支援樂觀鎖

 

當Aggregate被寫入資料庫以及從資料庫讀出的時候,Repository會負責設定它身上的version欄位。相關程式碼之前介紹Repository實作的時候已經看到,但當時Teddy並沒有解釋。現在再看一次,請參考圖3,首先看到GenericEventSourcingRepository的save方法,第52行透過eventSourcingStore儲存aggregateRootData之後,eventSourcingStore會更新aggregateRootData身上的version欄位。在第53行接著更新aggregate身上的version欄位。

接著看findById方法,第36行產生aggregate之後,第37行設定它的version欄位。

截圖 2022-07-01 上午12.12.36

▲圖3:GenericEventSourcingRepository更新Aggregate的version欄位

 

在資料庫層面,EventStoreDB本身就支援樂觀鎖,回憶一下圖4的程式碼,第36~36行設定expectedRevision,如此一來EventStoreDBClient在寫入資料的時候就會啟動樂觀鎖定檢查。

截圖 2022-07-01 上午12.22.13

▲圖4:EventStoreDB寫入時設定樂觀鎖定程式碼

 

至於以PostgreSQL所實做的Message DB也支援樂觀鎖定,但是如果把Message DB當成Outbox使用,則除了儲存領域事件以外,還需要儲存ORM資料,而ORM資料也有樂觀鎖設定的問題。在ezKanban中,把PostgreSQL當成Outbox使用的情況下,採用ORM的樂觀鎖機制,至於寫入領域事件則不做樂觀鎖檢查。

最後看一下ORM要如何設定樂觀鎖,請參考圖5。在TagData身上加上long version屬性(第28行),然後幫它貼上@Version annotation(第26行),如此便可啟動資料庫的樂觀鎖機制。

 

截圖 2022-07-01 上午12.31.37

▲圖5:TagData在version術性加上@Version即可自動啟動樂觀鎖

 

***

 

測試

講了老半天怎麼知道樂觀鎖到底有沒有作用?寫個測試案例就知道了,請參考圖6。第106行、107行讀出同一個Tag,分別放在tagV1與tagV2變數。第108行修改tagV1的名稱,然後儲存tagV1。此時資料庫中該Tag的版本已經被加1,但是記憶體中tagV2的版本號碼還是舊的。因此第112行儲存tagV2便會丟出樂觀鎖失敗例外。這個測試案例分別注入Event Sourcing與Outbox repository,執行結果都通過,如圖7所示。

 

截圖 2022-07-01 上午12.42.01

▲圖6:樂觀鎖測試案例

 

截圖 2022-07-01 上午12.46.54

▲圖7:測試案例執行結果

 

***

下集預告

Event Sourcing的基本知識介紹得差不多了,下一集開始介紹CQRS,等CQRS介紹完畢後在回頭談Event Sourcing的進階議題,例如快照與事件版本異動。

***

友藏內心獨白:終於要進入CQRS。

事件溯源(8):什麼是CQRS?

$
0
0

July 02 06:35~08:20

截圖 2022-07-02 上午7.35.47

▲圖1:ezKanban套用CQRS架構圖

 

前言

CQRSCommand Query Responsibility Segregation的縮寫,中文翻成「命令與查詢責任分離」。今天介紹CQRS的涵義以及它可以解決什麼問題。

 

***

起源

CQRS是由Greg Young所提出的設計模式,它的概念很簡單:分開設計以下兩種操作:會改變系統狀態但不會回傳值的操作,稱之為Command,以及不會改變狀態但會回傳值的操作,稱為Query。Greg Young同時也是Event Sourcing的提倡者,他在2011年開發EventStoreDB這個直接支援Event Sourcing與CQRS的資料庫,Teddy在<事件溯源(3):將Aggregate儲存至EventStoreDB>介紹如何用這個資料庫來做儲存領域事件。

***

CQRS有時又稱為簡稱讀寫分離,此概念並非Greg Young首創,最早由Eiffel語言與Design By Contract(DBC)發明人Bertrand Meyer所提出,稱為Command–Query Separation(CQS)。CQS的應用對象是物件,而CQRS則將範圍拓展到整個系統,包含API、Use Case、Domain Model、Database,通通可以讀寫分離。

這些資料網路上隨便查一下就找得到,聽過CQRS的鄉民可能都知道,但你知道Bertrand Meyer為什麼要提出CQS嗎?

CQS和DBC有關。在撰寫合約的時候,不管是preconditions或postconditions,都可能需要呼叫物件的method來做為狀態驗證。圖2是ezKanban系統中,Workflow Aggregate的deleteLaneById方法。ezKanban有套用DBC,第117行~119行是preconditions,第131行是postcondition。在第119行與131行中,呼叫getLaneById()方法檢查某個lane是否存在,getLaneById() 就是query,它回傳boolean但不會改變物件狀態。因為合約撰寫在物件身上,如果物件設計沒有遵守CQS,那麼你怎麼知道在合約中呼叫getLaneById()方法會不會不小心改變了系統狀態?如果沒有CQS,DBC就玩不下去了。

CQS提出至今已有30幾年,為什麼沒有大紅大紫,大部分的鄉民都是因為CQRS才知道CQS?答案很簡單,因為CQS的應用與DBC緊密相關,而大部分的開發人員並沒有實際應用DBC的機會,所以才會沒聽過CQS。

 

   截圖 2022-07-02 上午7.05.45

▲圖2:ezKanban系統Workflow的deleteLaneById方法

 

CQRS和DBC脫鉤,趕上分散式計算、微服務架構的大環境,拓展其應用的機會,所以才流行起來。

***

CQRS的好處

CQRS有以下三個主要的優點,簡稱為3S:

  • Simplicity:CQRS將系統分成Write Model與Read Model,這兩種模型的行為與責任大不相同。從單一責任原則(Single Responsibility Principle;SRP)的角度來看,CQRS進一步簡化不同模型之內的複雜度。
    • 寫入模型:
      • Strong consistency
      • normalized data model
      • one-way dependency
    • 讀取模型:
      • Eventually consistency
      • de-normalized data model (materialized view)
      • any-way dependency
  • Scalability:很多系統的讀取頻率遠大於寫入,例如在電子商務系統中大部分的使用者都在瀏覽資料,少部分的操作才會改變系統狀態。在這種情況下,套用CQRS可以單獨針對讀取部分加以拓展,如圖3所示。
  • Speed (Performance):綜合上述兩個優點,CQRS可以提升系統反應速度與效能,因為開發人員可以分別針對寫入端與讀取端採取不同的優化策略。例如,寫入端採取Event Sourcing簡化與加速寫入操作,讀取端因為不會改變狀態,可以用各種快取工具加快讀取。圖4是ezKanban團隊成員杜奕萱在她的碩士論文《套用命令與查詢責任分離以簡化聚合依賴:以 ezKanban 為例》中針對套用CQRS之後ezKanban的GetBoardContent查詢所做的效能測試,可以發現套用CQRS之後有著非常巨大的讀取效能提升。

 

截圖 2022-07-02 上午7.40.54

▲圖3:CQRS可分別拓展寫入與讀取服務

 

  截圖 2022-07-02 上午7.46.45

▲圖4:ezKanban GetBoardContent 效能測試

 

***

沒有缺點嗎?

以上把CQRS講的好像很神,它有沒有缺點?當然有。

雖然CQRS的套用可以先簡單的從API層與Use Case層開始,不一定要做到Domain Model甚至是資料庫的讀寫分離。但依據ezKanban這兩年套用CQRS的經驗,最終還是走向Domain Model與資料庫讀寫分離。將系統在各個階層「精細地」分成讀取模型與寫入模型,雖然個別模型內責任單一,變得比較簡單,但跨模型之間還是有相依性,要如何管理這些相依性就變成一個挑戰

例如,如圖1所示,ezKanban在資料庫端也套用CQRS,因此讀取資料庫與寫入資料庫之間的狀態同步是最終一致性。為了維持最終一致性,就會有新的設計工作產生:同步的訊息就需要考慮順序(ordering)與at least once等議題,而負責產生讀取資料庫的Projector(投影器)設計則須考慮idempotent與replay events等問題。另外,如何撰寫Projector也是一個問題。

換句話說,CQRS走到底的技術門檻會比較高。

這也是為什麼許多文章或書籍會建議不要為了套CQRS而套CQRS,要看自己的業務需求沒有有強烈到需要CQRS所帶來的這些好處。

***

下集預告

說明完CQRS基本概念,下一集先介紹ezKanbana套用CQRS之後對於簡化領域模型所達到的效果,之後再介紹ezKanbana如何實作Projector以及套用CQRS對於架構的影響。

***

友藏內心獨白:活用之後其實也沒有那麼難。

事件溯源(9):套用CQRS簡化領域模型

$
0
0

July 02 18:35~19:12;23:15~24:00;July 03 00:00~13:07

截圖 2022-07-02 下午6.43.09

▲圖1:ezKanban Core Domain Model(簡化版)

 

前言

上一集提到CQRS可以簡化設計,這一集以ezKanban core domain為例,說明套用CQRS之後如何簡化原本領域模型之間Aggregate(聚合)的關係。

 

***

雙向關聯造成不必要的複雜度

圖1是ezKanban套用CQRS之前Core Domain的領域模型簡化版,一共有四個Aggregate:Board、Workflow、Card、Tag。其中有兩對Aggregate保持雙向關聯,分別是:

  • Board與Workflow:一個Board可以有多個Workflow,而且必須記錄每一個Workflow在Board上面的順序(order)。此外,Workflow身上紀錄boardId,讓它知道自己屬於哪一個Board。為了記錄Board身上每一個Workflow的順序關係,Board身上有List<CommittedWorkflow>屬性,其中CommittedWorkflow是一個association class,身上有boardId, workflowId, order這三個屬性。
  • Workflow的Lane與Card:一個Lane上面有多張Card,而且必須記錄每一張Card的順序(order)。此外,Card身上也紀錄著workflowId與laneId,讓它知道自己屬於哪一個Workflow的哪個Lane。為了記錄Lane身上每一張Card的順序,Lane身上有List<CommittedCard>屬性,CommittedCard也是一個association class,身上有cardId, laneId, order這三個屬性。

 

接下來以Board與Workflow的關係為例,說明這個雙向關聯對於領域模型造成什麼影響。請參考圖2,為了維持這個雙向關聯,Workflow與Board之間需要狀態同步,CreateWorkflow之後,需要通知Board在它身上加入這個Workflow。在DDD中,Aggregate之間的狀態同步是「狀態最終一致性」。換句話說,為了維持這個雙向關聯,領域模型的實作變得比較複雜(需要維持狀態最終一致性)。

 

截圖 2022-07-02 下午11.24.30

▲圖2:為了維持雙向關聯Workflow與Board必須達成狀態最終一致性

 

仔細想一想,為什麼Workflow與Board之間需要維持雙向關聯?因為Board需要知道它身上有多少個Workflow,以及這些Workflow的順序。繼續追問下去,那麼為什麼Board需要知道它身上的Workflow與順序?是Board的業務邏輯需要這些資訊嗎?完全沒有。這些資料是為了顯示用途而存在。如圖3所示,ezKanban的GetBoardContent顯示Board裡面有3個Workflow。也就是說,為了顯示(查詢)用途而導致領域模型增加不必要的複雜度。

 

截圖 2022-07-02 下午11.42.06

▲圖3:包含三個Workflow的Board畫面

 

***

Eric Evans怎麼說

在《Domain-Driven Design: Tackling Complexity in the Heart of Software》書中作者Eric Evans提到:

It is important to constrain relationships as much as possible. A bidirectional association means that both objects can be understood only together. When application requirements do not call for traversal in both directions, adding a traversal direction reduces interdependence and simplifies the design. Understanding the domain may reveal a natural directional bias. (盡可能地限制關係很重要。雙向關聯意味著兩個物件只能一起理解。當應用程式不需要雙向遍歷時,採用單向遍歷可以減少相互依賴並簡化設計。了解(問題)領域可能會揭示一種自然的方向偏差。)

如果在問題領域中單向依賴就可以解決問題,在領域模型中就不需要維持雙向依賴。一開始ezKanban並沒有套用CQRS,所以它的領域模型很自然地需要同時滿足寫入(Workflow身上有boardId)與讀取(Board身上有List<CommittedWorkflow>屬性)的需求。在傳統物件導向分析與設計(OOAD)中,因為沒有Aggregate的觀念,所以這種雙向關係並不會造成什麼大問題。因為Board與Workflow直接可以透過記憶體參考而存取對方,因此Workflow狀態改變時Board立即可得知,不需要透過領域事件做到狀態最終一致性。但是在DDD中,因為ezKanban把Board與Workflwo設計成兩個不同的Aggregate,所以這種混合寫入與讀取的單一領域模型,從寫入的角度來看,便產生不必要(透過領域事件達到最終一致性)的複雜度。

Eric Evans在書中提到一些簡化關聯的做法,但是並沒有從讀寫分離的角度來探討如何簡化領域模型的關聯。

***

套用CQRS簡化模型複雜度

Teddy剛剛分析過,Board身上的List<CommittedWorkflow>是為了查詢而存在,寫入模型並不需要這個資料結構。套用CQRS之後,圖1中ezKanban領域模型的兩個為了讀取模型而存在的association class(CommittedWorkflow與CommittedCard)就可以直接拿掉,如圖4所示。

簡化後的寫入模型,省去了不必要的關聯,也去除了不必要的狀態最終一致性。

 

 截圖 2022-07-03 上午12.06.28

▲圖4:ezKanban套用CQRS之後的寫入模型

 

但是問題來了,ezKanban還是需要知道Board身上有多少個Workflow,以及這些Workflwo的順序。在寫入模型中拿掉List<CommittedWorkflwo>之後,這個資料要從哪裡來?這就要靠CQRS的Query Model來記錄這個關聯性,如圖5所示。

 

截圖 2022-07-03 上午12.26.42

▲圖5:ezKanban套用CQRS之後的讀取模型

 

現在剩下最後一個問題:「怎麼產生讀取模型所需的資料?」請參考圖6,在讀取模型中必須撰寫一支用來在讀取資料庫中產生Read Model所需資料的Projector程式。在它會監聽Write Model所發出的領域事件,然後依據這些領域事件在Read Database中投影出Read Model所需的資料。這種資料被稱為「非正規化」或「物質化」資料,為了快速讀取可以允許重複的資料存在。

以ezKanban為例,GetBoardContent查詢所需的資料是一個代表整個Board所有資料的JSON物件,存在PostgreSQL資料庫的jsonb欄位。GetBoardContent查詢只需要下一個SQL指令就可把整個Read Model所需的資料從PostgreSQL讀出來,不需要下任何的join條件,所以查詢的速度很快。

 

截圖 2022-07-03 上午12.31.28

▲圖6:ezKanban套用CQRS之後的架構圖,箭頭方向代表data flow

 

在這裡有兩個重點要注意,首先Write Database與Read Database之間的狀態是最終一致性,也就是說Read Model不一定會有最新的資料,這一點在系統設計時被需要考量進去,否則可能會造成使用者體驗不佳。例如,使用者剛剛才下一筆訂單(存在於Write Database中),但在訂單查詢畫面(從Read Database讀取)中卻查不到這筆訂單的資料。所以Teddy常說CQRS雖然簡化寫入與讀取模型內部的複雜度,但卻把複雜度轉換成兩個模型之間狀態同步的問題。至於如何取捨,就要看實際的業務需求與應用情境而定。

第二個重點是,這支Projector程式雖然是屬於Read Model,但它做的工作是「產生Read Model」,也就是說它是負責寫入Read Model的人。它的程式邏輯,可能有部分,甚至很多,和Write Model的Aggregate身上的邏輯互相重複。另外,因為它可能收到重複的領域事件(在分散式系統中,事件傳遞通常只能滿足at least once,不容易做到exactly once),因此它需要滿足idempotent,否則可能投影出錯誤的Read Model。最後,因為Read Model可能因為某些原因導致本身的狀態錯誤或是被刪除,因此Projector必須有能力能夠從頭重建Read Model,通常是藉由replay所有相關的領域事件來達到此功能。

 

***

下集預告

看完CQRS的概念說明,下一集將介紹ezKanbana如何實作Projector,在PostgreSQL資料庫儲存Read Model。

***

友藏內心獨白:頭快爆炸了嗎XD。

Viewing all 1089 articles
Browse latest View live