11

領域驅動設計學習筆記(14):透過合約讓領域模型與通用語言更精確

 3 years ago
source link: https://teddy-chen-tw.blogspot.com/2021/03/14.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

領域驅動設計學習筆記(14):透過合約讓領域模型與通用語言更精確

March 18 21:18~23:13

ACtC-3fNlhY-57hoJFOFwCycZnGZ5kPa7BPY9dhIQhYbzrepNl9cFfrBBjB4qOD3d6oix1JfpT4Q6qdGZZI9ig3orT-x9pEMFwNQn5CyyENAB-toeo4C4h1AQa-wSPm9a6gSxeMcC1xgUivdRNOvsAhrFjYnmA=w2152-h930-no?authuser=0

【圖1】ezKanban的design-level event storming(部分)

工商服務

自學好久的領域驅動設計(Domain-Driven Design;DDD)卻連一個完整的小功能都做不出來嗎?覺得DDD要學得東西太多,總是抓不到重點嗎?想要透過事件風暴(event storming)來塑模業務流程並找出bounded context與aggregate,卻不知如何下手嗎?

你需要真實世界的DDD + Event Storming + Clean Architecture 教學與範例,歡迎參加2021年4月2、3、4日(五、六、日)的【領域驅動設計與簡潔架構入門實作班】課程(已確定開課)。

前言

在〈用合約彌補測試案例的不足(上):撰寫合約〉與〈用合約彌補測試案例的不足(下):透過合約發現系統行為缺陷〉這兩篇文章中,Teddy談到依合約設計(Design By Contract)在ezKanban系統中的幾個實際應用案例,這一集Teddy要談如何透過撰寫合約找到領域模型與通用語言的行為漏洞。

問題敘述

幾天前Teddy去客戶家上DDD與Event Storming企業內訓課程,沒有和ezKanban團隊mobbing。今天mobbing的時候團隊問Teddy一個關於Aggregate行為的問題,請先參考圖1:

  • WorkflowCard是兩個Aggregate,前者代表看板的工作流程,後者代表一項工作。
  • Workflow包含垂直的工作階段—Stage,與水平的工作階段—SwimLane
  • 使用者新增Card之後,Card被放在Workflow的Stage或SwimLane裡面,使用者可以調整卡片的順序(order),該順序紀錄於Stage和SwimLane身上。
  • 由於Workflow與Card分屬不同的Aggregate,因此當使用者移動卡片(Move Card)之後,Card必須通知Workflow卡片已經移動,以同步Workflow身上關於卡片位置與順序的狀態。

團隊遇到的情況是:他們在寫Card.move() 的前置條件(如圖2),原本希望除了檢查參數不是null,以及order >= 0 以外,還能夠檢查order不會大於該卡片所要加入的那個Stage/SwimLane的卡片總數。例如,使用者在To Do這個Stage加上一張卡片,To Do原本已經有2張卡片,move收到的order參數是8,則前置條件檢查失敗。團隊希望能在Card.move()的前置條件檢查這個限制。

ACtC-3cQe5wAe204qyI7oPqCMOvHL15KCkmv8SB4jw6viVBttxdDy8DoKvnOn8JCjwPVAzS0SzV5OHo7Lqbf48Lq7irTeUu5cSirGBUSuHxBd5Gx8KEKlmy6A6QrYAV8LOKU58a5i4KKhVJmxA9syES2NRxqww=w2800-h478-no?authuser=0

【圖2】Card.move方法的前置條件

但是,因為一個Stage/SwimLane身上有多少張卡片的資料是儲存在Stage/SwimLane身上,Card並不知道,所以也就沒辦法在Card.move()方法加上這一條前置條件。

於是團隊問Teddy:「是不是應該把move方法移到Workflow上面,這樣就可以在前置條件中檢查Card的順序?」

思考邏輯

這個問題,可以從以下兩點來思考:

  1. Single source of truth:Card與Workflow分屬不同的Aggregate,兩者之間透過領域事件達到狀態最終一致性。因此,第一個思考點就是:「關於一張卡片移動至哪個Stage/SwimLane這件事,擁有第一手資訊的人是誰?Workflow還是Card?」Teddy覺得應該是Card。
  2. 關於檢查Card順序這件事,除了原本的Card.move()方法以外,另外一個地方就是Workflow.commitCardInLane這個方法,請參考圖3第270~271行,檢查了卡片的順序不能大於現有Stage/SwimLane裡面所擁有的卡片數量。
ACtC-3fQ0tggy15ByQMqWnnjTSp14wDdIF7EJ7_Php926F_PxeIdE5pc246aKlM1B1CejfFAI_Rox3QYr4TBJIhjf0ZfOt3InK--TN17UfjuBbvmoSP6jQbYA5PGq0uF_0HPbLpozxzcVzp_FeLKPy5H8HYy8A=w2302-h716-no?authuser=0

【圖3】Workflow.commitCardInLane()方法的前置條件

現在假設一個情況:

  • 一張卡片要移到To Do上面的第8個位置。
  • To Do身上只有2張卡片。

因此,圖3第270~271行的前置條件會失敗,丟出PreconditionViolationException並且終止程式執行,代表呼叫它的人有bug。呼叫commitCardInLane的人是誰?是CardMoved領域事件發生後的Policy,簡單來說就是Card有bug,這個bug很可能是前端傳過來錯誤的order。

如果不希望因為卡片順序有問題就終止整個系統執行,另一種可能性是放寬Workflow.commitCardInLane()的前置條件,把270~271行拿掉,改成在程式裏面(commitCardInLane的method body)判斷,如果條件不成立則丟出一個CardCommittedFailed領域事件,並通知Card執行交易補償(因為Card已經把它自己的狀態改成移動到新的To Do裡面,但Workflow判斷卡片所移動的順序是不合法的,所以請卡片移回它原本的位置)。

如果選擇這種作法,則原本圖1要再加上CardCommittedFailed領域事件,如圖4所示。

ACtC-3eZLmsrHbs5otomDaxE7jSN24pTb4XwvUaGWbxFODr4rvZ1HlM_TUeTPCpPTwzhok56BkSGynykw9MBk3dmgZMOFMSDp4U2HQfOXfDivilfc5RQWg2aDeHZpwYaxoZ70bm3Rw2e_K6oqxz7etWw-RLHzw=w1716-h884-no?authuser=0

【圖4】讓領域事件以及Workflow Aggregate與Card Aggregate的行為更加完整。

結論

無論是選擇上述兩種方式的哪一種,系統的行為都因為撰寫合約而變得更精確。最後團隊與Teddy選擇了第二種方式--交易補償,因為這種行為模式容錯能力比較好。

看到這裡如果還沒跑開的鄉民,應該可以發現以下結論:

透過撰寫合約讓領域模型與通用語言更加完整。

友藏內心獨白:寫合約可以找到系統行為的漏洞。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK