l

2021年6月24日 星期四

領域驅動設計學習筆記(18):學DCI,重構Aggregate ,Part 3

June 24 16:20~17:14

▲圖1 :在Aggregate Root身上增加這兩個方法以支援動態扮演角色的功能


前情提要

今天是表定介紹DCI與Aggregate連載的最後一集,談如何讓Aggregate具備動態扮演不同角色的能力

第一集Teddy提到在DDD的情境之下,Aggregate本身就是若干個小規模使用案例的集合體,所以實務上Teddy認為並不需要像DCI架構所建議的在不同的Context底下,讓Data object動態扮演不同的Role以執行不同功能。但為了學習目的,Teddy還是想辦法讓Aggregate具備這種動態扮演角色的能力。

***

透過Java Reflection實作

ezKanban後端以Java語言開發,Java是一個靜態型別語言,要讓物件動態扮演不同的角色,大概只能透過Reflection機制動手腳。

Teddy的想法很簡單,在Aggregate Root身上增加圖1的兩個方法:

  • mixinBehavior:把一個物件加入behaviors list裡面,這個物件可以是任意型別的物件。
  • playRole:想讓Aggregate扮演某個角色需要先把該角色透過mixinBehavior方法加入Aggregate Root,再呼叫playRole方法,傳入參數為扮演角色的類別,這樣就可以執行該角色身上的方法。請參考圖2。


▲圖2:測試案例,展示如何動態加入角色到Board Aggregate Root

以下解釋圖2程式碼:

  1. 宣告一個任意的類別叫做NewBehavior,它不用實作任何介面,就是一般的Java Class即可。它的身上有一個getBoardInfo方法,回傳一個board id 加上 board name所組合成的字串。這個類別需要獲得Aggregate Root的資料,因此它需要撰寫一個初始化的方法讓Aggregate Root呼叫,並且幫這個初始化方法貼上@MixinInitializer這個annotation。
  2. 呼叫board.mixBehavior,把newBehavior當成參數傳給board。這一行等於在程式執行期間動態指派一個角色給Board Aggregate。
  3. 呼叫board.playRole(NewBehavior.class)讓board扮演這個新的角色。
  4. 最後,可以呼叫到NewBehavior身上的getBoardInfo方法。

▲圖3:測試案例執行結果,當然是綠燈通過


完整的程式還有不少細節Teddy沒有在這裡逐一解釋,不過大方向基本上就是上述的過程。

***

結論

DCI是一個很有趣的架構,而且可以和DDD與Clean Architecture一起使用,讓程式碼變得更乾淨,這是沒有問題的。

由於Aggregate Root實際上某種程度就是一個小的DCI,所以是否需要動態指派角色給它,Teddy目前還不是很確定是否有這個需要,為了學習的目的Teddy還是花了點時間完成這個功能。

最後,Teddy還在持續探討將DCI應用在更大的情境的可行性,例如在Saga pattern中使用DCI,等有結果之後再跟鄉民們分享。

***

友藏內心獨白:動態扮演角色真的滿酷的。

2021年6月23日 星期三

領域驅動設計學習筆記(17):學DCI,重構Aggregate ,Part 2

June 23 10:03~12:28

▲圖1:Board Bounded Context領域模型,只表示Board與Workflow這兩個Aggregate的關係。


前言

上一集介紹DCI架構以及它與Clean Architecture與DDD Aggregate Root的比較,今天說明如何藉由DCI的精神來重構Aggregate Root。Teddy將以ezKanban系統中,Board Bounded Context的Board Aggregate Root為例,介紹這個重構過程。

***

重構前的Board Aggregate

▲圖2:Board重構前程式碼片段,完整的程式有七百多行


圖1為Board Aggregate類別圖,圖2為Board重構前的程式碼片段,顯示其資料成員:

  • id:繼承自AggregateRoot物件,紀錄Board id。
  • nam:紀錄Board的名稱
  • teamId:紀錄Board屬於哪個Team
  • boardMembers:紀錄Board身上有哪些成員。
  • committedWorkflows:紀錄Board身上有哪些Workflow。Workflow是另一個Aggregate,代表看板上的工作流程。
  • boardSessions:一位使用者進入Board之後會建立一個board session,透過board session讓位在同一個Board的不同使用者可以互動。

DCI架構要求物件不需要太聰明,只需是Data object即可,至於原先物件身上的方法則抽離出來定義在Role(角色)物件身上。接下來說明如何將Board的角色與資料抽離出來。


***

定義Role介面

根據Board所操作的資料,Teddy發現Board扮演了五種角色:

  • Content:Board的內容管理,包含board id, board name。
  • Dependency:管理Board與其他Aggregate的相依性,在此範例中只有team id。
  • Membership:Board成員管理,可以新增、刪除、查詢成員。
  • Workflow Relation:Board與Workflow關係管理,可以新增、刪除與調整Workflow順序。
  • Collaboration:管理Board身上的同時上線成員。

接著新增KanbanBoardRoles interface,在裡面定義這五種角色,如圖三所示。

▲圖3:用Java interface定義Board的五種角色

***

定義Data介面

定義好角色之後很清楚發現每一種角色只使用到原本Board身上的特定資料成員,因此圖2宣告資料成員的方法很明顯的違反了介面隔離原則。所以針對每一種角色使用到的資料,在該角色內再定義一組存取資料的State介面,如圖4所示。


▲圖4:在每一個角色介面內定義一個存取資料的State介面


最後宣告一個BoardState介面,讓它繼承每一個定義在Role裡面的Role-Based State介面,如圖5所示。

▲圖5:宣告一個包含所有Board資料的BoardState介面,要用它來取代原本宣告於Board身上的資料成員

***

實作角色

角色介面與資料介面都定義好,就可以撰寫實作角色的程式碼。說是實作但其實並非重頭開始寫,而是重構原本的Board Aggregate。重構的方式很簡單,把原本已經寫好放在Board身上分屬於不同角色的方法,搬移到各個角色身上就差不多大功告成了。Teddy把實作角色的類別稱為Behavior類別,圖6為MembershipBehavior類別程式片段,原本joinAs方法放在Board身上,重構方式只是把它從Board身上移到MembershipBehavior身上。

由於joinAs方法原本定義在Board身上,會存取Board資料與方法,也就是說joinAs依賴Board。資料的依賴關係由第24行的State介面所隔離,行為的依賴則是由第23行MixinContext類別所隔離。State介面已於前述段落中說明,MixinContext留待下一集說明如何讓Data object支援動態扮演角色的時候再解釋。


▲圖6:MembershipBehavior類別實作Membership角色介面

***

重構Board

每一個角色都實作完畢之後,最後一步就是重構Board類別。參考圖7:

  • 23~26行:讓Board實作圖3所定義的五個角色,以便讓它對外維持原本的介面。
  • 28行:用BoardState取代原本的資料成員,此時BoardState相當於DCI的Data。
  • 29~32行:圖3定義了五個角色,其中DependencyRole因為只有一個getBoardId方法,所以直接實作在Board身上。其他四個角色各用一個類別實作。29~32行宣告這四個角色的資料成員,Board採用composition-deligation的方式,將客乎端呼叫Board身上不同角色的方法,轉呼叫個別的角色實作物件,請參考圖8。


▲圖7:重構後的Board資料成員


▲圖8:Board身上的joinAs方法轉呼叫membership角色的joinAs方法

重構完畢後Board程式碼剩下約190行(原本有7百多行)。

***

結論

依據DCI「精神」重構之後,Board Aggregate的責任與介面變得很乾淨,個別角色所負擔的責任其實做程式碼也跑到相對應的行為類別身上,符合單一責任原則與介面隔離原則。可以針對個別行為單獨撰寫單元測試,增加了可測性。

DCI架構還有一個很神奇的地方,就是希望Data object可以在不同的Context(使用案例)中動態扮演不同的角色,以執行不同的功能。Teddy將於下一集說明如何用Java語言在DDD Aggregate中實作這個動態扮演角色的特性。

***

友藏內心獨白:真,單一責任原則。

2021年6月22日 星期二

領域驅動設計學習筆記(16):學DCI,重構Aggregate ,Part 1

June 22 09:28~11:08


DCI簡介

最近三個月陸陸續續花了點時間研究DCI架構(Data, Context, Interaction),該架構包含三個主要元素:

  • Data:資料物件,傳統OOAD或DDD所謂的領域模型物件。DCI認為物件只需要有資料,以及存取資料的簡單方法即可。操作這些資料的演算法(methods),應該從物件身上獨立出來,放到角色(Role)身上。從這個角度來看,Data類似Martin Fowler所說的貧血模型(Anemic Domain Model)。
  • Context:實作使用案例的地方,DCI認為,物件的演算法必須放在一個特定使用情境去討論才可知道它的意義,這個情境(Context)就是使用案例。
  • Interaction:使用案例藉由指派若干物件扮演不同的角色,並協調物件之間彼此的協作來完成使用案例,稱為互動(Interaction)。角色是一個沒有狀態的物件,有點類似domain service。但在DCI架構中,Data物件可以在程式執行期間動態扮演不同角色以便獲得執行不同演算法的能力。Data物件扮演某個角色之後,該Data物件看起來就變成該角色的型別,可以執行該角色身上所定義的方法。

乍看之下DCI好像把原本富有行為的充血模型,搞成只剩下資料的貧血模型,這不是一種物件導向分析設計的壞味道嗎?其實不然,DCI的倡導者James Coplien認為:「把資料和行為封裝在類別身上是祖父級的物件導向設計方法。」DCI的Data物件在編譯期間雖然沒有行為,但其行為是在執行使用案例時動態由Context所指派。Coplien認為唯有給定一個Context再去解讀物件身上的行為才會有意義,程式也比較容易被讀懂。

***

DCI與Clean Architecture比較


▲圖1:Clean Architecture與DCI對應關係


如圖1所示,把DCI對應到Clean Architecture,Teddy認為Data與Role的實作應該是位在Entity Layer,Context與Interaction則是位於Use Case Layer。

由於Clean Architecture並沒有特別規範Entity Layer的領域物件需要如何實作,所以如果開發人員願意,當然可以用Data與Role來實作領域模型。這就好比可以用DDD的Aggregate、Entity、Value Object、Domain Service來實作Clean Architecture的Entity Layer是一樣的道理。

***

DCI與DDD比較

把DCI和DDD實作融合在一起花了Teddy比較多的時間,主要的問題在於DDD的Aggregate設計模式。Aggregate是一組物件的集合,從中指派一個物件當作Aggregate Root,所有對於該Aggregate內部物件的存取,都需要透過Aggregate Root。Aggregate同時也是交易邊界,同一個Aggregate資料存取必須發生在同一個交易之內,不同Aggregate則是透過領域事件同步狀態,達到最終一致性及可。

若將Aggregate對應到DCI,它到底屬於Data,還是Context?

看到這裡有人可能會覺得:「Aggregate不是放在Entity Layer的物件嗎,那就應該屬於Data啊。」在Teddy實作DDD的時候,的確是把Aggregate放在Entity Layer,但實際上Aggregate Root某種程度扮演了「多個小型使用案例」的角色。因為在設計階段決定Aggregate邊界的時候,很大部分的原因是依據「業務範圍」來決定Aggregate大小。也就是說,從業務需求的角度,若干物件的協作必須發生在同一個交易內商業規則才不會被違反,因此把這些物件放在同一個Aggregate。這樣聽起來,是不是和DCI的Context所扮演的角色幾乎一樣? 主要的差別在於,Aggregate是靜態的物件協作範圍,DCI的Context則是動態的物件協作範圍。

***

下集待續

先把背景知識介紹完,下一集Teddy將說明如何參考DCI的精神重構ezKanban系統的Aggregate。重構之後的程式變得更乾淨,符合單一責任原則與介面分隔原則。

***

友藏內心獨白:天下事分分合合。




2021年6月1日 星期二

4.99人民幣的學習

June 01 12:48~13:28

▲James Coplien的DCI演講


最近Teddy對DCI(Data, Context, Interaction)架構很感興趣,花了點時間在研究它。相較於其他軟體架構,DCI算是非主流架構,網路上的資料也比較少,主要來自於James Coplien所寫的《Lean Architecture: for Agile Software Development》以及他的幾個演講(可在YouTube上用DCI搜尋)。

前幾天晚上Teddy找到一個張曉龍(中興通訊資深軟件架構師) 在ArchSummit深圳2019的中文演講,講題是〈當DDD遇上DCI〉,引起Teddy的興趣。該演講的錄影被放在中國的「極客時間」上當作一個課程販賣,人民幣4.99。

Teddy沒有微信也沒有淘寶支付,無法購買此課程。後來託Erica請他的中國朋友幫忙,好不容易才買到這個課程,差點上演4.99元逼死英雄好漢的戲碼 XD。

雖然Teddy對DCI研究還不深入,但聽完這個演講,看到演講分享的實作,感覺講者有點胡搞瞎搞。有幾個很明顯的問題:

  • 亂用Context:DCI的C是Context,相當於Use Case,也就是Application Service,但講者卻想要用Context來進一步切割Aggregate,並且將C用DDD的Domain Service實作。這一點Teddy覺得有點「張飛打岳飛,打得滿天飛」的感覺。在講者所舉的例子中,除了造成不必要的人為設計複雜度以外(稍後說明),實在看不出什麼好處。
  • 未透過領域事件解偶:講者開口閉口解耦合、提高內聚,但範例程式本身就把「轉帳」和「傳遞訊息到手機上面」這兩件事耦合在一起啊。在DDD裡面,這靠Domain Event去解耦合,但講者在拆分Aggregate的時候,不但方式很怪,也壓根沒提到Domain Event,彷彿Domain Event不存在一樣。
  • 亂拆角色:講者對於Role(角色)的濫用與誤用到達一個讓人匪夷所思的境界,如圖1所示,講者以一個銀行轉帳的例子來說明他如何在DDD中套用DCI。他讓LocalAccount這個Aggregate Root扮演MoneyCollector, LocalMoneySrc, LocalMoneyDest, AccountInfo, Balance, Phone這五個角色。先不管這些角色的名字取的有多爛,Balance代表帳號餘額,光是Balance被獨立的一個角色這件事就非常詭異。其次,Phone這個角色存在的目的是為了轉帳成功之後通知使用者,為了具備通知的能力,一個LocalAccount需要去扮演Phone,這不是特別詭異嗎?LocalAccount去使用(use)Phone即可,為什麼要去扮演一支電話?難道你為了喝牛奶,需要扮演乳牛的角色嗎?
  • 未反映真實事件:Teddy不是銀行這個領域的專家,但轉帳這件事,很明顯的不會只牽涉到來源帳號與目的帳號,至少中間還會有一個Bank的角色。講者號稱要使用DCI來幫助DDD切割Aggregate,但他切了一堆有的沒得角色,反倒是這個重要的Bank角色完全沒有被提到,這不是瞎忙一場嗎?


▲圖1:張曉龍在演講中所展示套用DCI之後的領域模型

***

正確示範

Teddy後來在〈An Empirical Study on Code Comprehension: Data Context Interaction Compared to Classical Object Oriented〉論文裡面看到一個比較像樣的DCI程式範例,如圖二所示。但這樣寫有沒有比較好,要再花時間想一下。

***

友藏內心獨白:胡搞瞎搞,勇氣可嘉。

2021年4月27日 星期二

領域驅動設計學習筆記(15):領域專家缺貨

April 27 17:12~17:48

▲David West提到在1968年代,銀行的開發人員就是領域專家


不少朋友問Teddy一個問題:「Event Storming工作坊的時候如果領域專家不願意參加,或是勉強參加但卻都在擺爛,被動等著開發人員去問他問題,這樣該怎麼辦?」

這是一個很常見的問題,因為許多公司,特別是大公司,雖然嘴上說導入敏捷好幾年了,但骨子裡還是傳統瀑布是開發方法,客戶(業務單位)與開發人員還是透過中間人所產生的文件來溝通。這個中間人可能是Scrum裡面的Product Owner(PO),或是傳統的專案經理或是系統分析師/商業分析師。客戶(業務單位)認為開發軟體是開發人員的事情,能講的他們都已經講完了,這些開發人員不要三不五時就跑來煩他們,趕快把東西生出來。

另一方面,客戶(業務單位)理論上雖然代表著領域專家的角色,但這並不表示他們有能力和開發人員一起析分商業流程,甚至有些商業邏輯的細節連他們也不十分清楚。再加上客戶(業務單位)的人日常工作量已經很多,自己的事都忙不過來,很難要求他們長時間配合開發團隊持續進行Event Storming並精煉領域知識以及共通語言。

***

所以,如果你的開發團隊有願意一起長時間配合的領域專家,恭喜你你的現況離理想狀況比較接近。如果沒有,也不要灰心喪志,日子還是要過下去。

領域專家「缺貨」怎麼辦?如果真的調不到「貨」,只好讓自己變成領域專家。

幾天前Teddy聽了David West的〈 The Past and Future of Domain-Driven Design〉演講,講者提到在1968年代,銀行的開發人員就是領域專家。在【搞笑談軟工臉書社群】上,有兩位朋友也分享了類似的經驗。


所以,萬一開發專案中真的沒有領域專案,而案子也還是要做下去,考慮訓練自己成為領域專家。這樣做一定會花費比較多的時間,走更多的冤枉路,但總是能緩慢往前移動,總比留在原地一動也不動來的好。

就算團隊中有領域專家,開發人員也不能完全信任領域專家,自己還是要動腦思考,畢竟要將問題領域(problem domain)轉化成解法領域(solution domain)的人,是開發人員,不是領域專家,開發團隊還是要多擔待一些。

***

友藏內心獨白:一個人或是一個團隊的End-To-End,就可以變得很靈活。

2021年3月18日 星期四

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

March 18 21:18~23:13

【圖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()的前置條件檢查這個限制。


【圖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裡面所擁有的卡片數量。

【圖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所示。


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

***

結論

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

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

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

***

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

2021年3月10日 星期三

用合約彌補測試案例的不足(下):透過合約發現系統行為缺陷

March 10 14:28~16:05

【圖1】ezKanban領域模型(部分)


前情提要

上集〈用合約彌補測試案例的不足(上):撰寫合約〉提到兩個撰寫合約的例子,今天介紹透過撰寫合約發現ezKanban系統bug的實際案例。

***

commitCardInLane的測試案例

在講解透過合約找到系統行為bug之前先介紹ezKanban的Board bounded context的部分領域模型,如圖1所示。圖1包含兩個Aggregate,Workflow與Card:

  • Workflow是Aggregate Root,它包含多個Lane。
  • Lane有兩種,垂直的叫做Stage,水平的稱為Swimlane
  • Card是Aggregate Root,它代表看板上面的一張卡片。Card放在某個Lane上面,兩者之間的關係用CommittedCard表示。

Workflow身上有一個commitCardInLane方法,表示某張卡片被放入某個Lane,其介面如下:

public void commitCardInLane(CardId cardId, LaneId laneId, int order, String userId)
  • cardId:要放入的card Id
  • laneId:要放在哪個lane上面的lane id
  • order:順序
  • userId:執行該操作的使用者 id

ezKanban採用TDD方式開發,針對commitCardInLane已經有一個測試案例如下:

【圖2】commitCardInLane的單元測試


由於workflow.commitCardInLane是一個很簡單的方法,它的實作類似以下兩行程式:

Lane lane = getLaneById(cardCommitted.getLaneId()).get();
lane.addCommittedCard(cardCommitted.getCardId(), cardCommitted.getOrder());

因為卡片實際上是放在lane上面,因此還需要幫lane.addCommittedCard寫一個測試,如圖3所示。


【圖3】這個測試案例規範一個行為—卡片的順序等於它被加入lane的順序。

寫完這兩個測試案例,感覺就差不多了,程式跑起來也都沒什麼問題。

***

commitCardInLane被疏忽的行為

再看一次圖4的commitCardInLane介面:

【圖4】commitCardInLane介面


當ezKanban團隊與Teddy幫commitCardInLen撰寫合約的時候,首先寫出以下幾個沒爭議precondition (前置條件):

  • car id is not null
  • lane id is not null
  • order >= 0
  • user is is not null
  • 要加入的那個lane必須存在

接著想到,既然卡片是要放到某個lane裡面,因此還需要一條:

  • 要加入的card 不存在lane id所屬的那個lane裡面

上述precondition只規範了同一張卡片不能同時存在同一個lane裡面。寫完之後又想到另一個問題:「卡片可以同時存在不同的lane裡面嗎?」不行,一張卡片只能出現在一個lane裡面,所以剛剛那條precondition寫得不夠精確,要改成:

  • 要加入的card 不存在所有的lane裡面


最後的precondition如下圖所示:

【圖5】commitCardInLane的preconditions


加上圖5的preconditions之後,就可以確保在這些前置條件滿足的情況之下,加入一張卡片之後,它就一定會依據正確的順序被放入正確的lane裡面。

最後,補寫一個單元測試用來確認剛剛最後一條前置條件所規範的行為,參考圖6。

【圖6】補寫測試案例來記錄由合約所發現的系統行為

***

討論

看到這裡,有經驗的鄉民可能會說:

從TDD/Specification by Example(實例化規格)的角度,「要加入的card 不存在所有的lane裡面」的這一個系統行為也是可以被找出來啊,只要在實例化規格的時候列舉出這個例子不就好了?根本不需要寫合約也可以發現。

這樣講「理論上」是沒錯的,但實際上:

  • 在實例化規格「找例子」的時候,也是有優先順序之分。團隊通常會把與領域專家開會的寶貴時間拿來討論最重要的系統行為。像是commitCardInLane(把一張卡片放到lane裡面)這種有點重要又不是太重要的行為,能夠想出「卡片要成功放入lane」以及「卡片的順序要和放入的順序一致」這兩個例子,已經算及格了。在有限的時間與壓力之下,在想例子的當下不見得很容易就想到「一張卡片不能重複放在多個lane」這個例子。
  • 寫合約就不一樣,因為人只要講到「簽訂合約」,立刻會從「君子模式」切換到「小人模式」。針對合約內容,能多挑剔就多挑剔,除了保護自己不要上當以外,還要幫自己取得最大利益。因此,光是從method介面,就很容易可以想出很多系統行為的「邊界條件」。

寫合約還有一個好處,它不需要像測試案例一樣,為了讓測試案例跑起來,需要:

  • 準備測試資料
  • 執行待測程式
  • 比對測試資料

寫合約只需要……寫合約。就是用一段程式碼描述程式的行為即可,這段程式碼一開始甚至可以是空的,只需要有名稱即可。例如圖5的205行card_not_committed_to_any_lay()用來代表「一張卡片不能重複放在多個lane」的前置條件。先把合約的名稱(要描述的系統行為)想好,在撰寫合約的檢查內容即可。

***

工商服務

在套用領域驅動設計的時候,你的物件導向領域模型都生不出來嗎?針對領域模型的狀態正確性感到很棘手嗎?你想透過撰寫合約來增加領域模型的正確性嗎?

你需要補充基本的物件導向養分,歡迎參加2021年4月20日(週二)的【敏捷開發懶人包:物件導向技能】一日課程。

***

友藏內心獨白:合約就是用規格取代舉例。

用合約彌補測試案例的不足(上):撰寫合約

March 09 23:05~March 10 00:55


工商服務

在套用領域驅動設計的時候,你的物件導向領域模型都生不出來嗎?針對領域模型的狀態正確性感到很棘手嗎?你想透過撰寫合約來增加領域模型的正確性嗎?

你需要補充基本的物件導向養分,歡迎參加2021年4月20日(二)的【敏捷開發懶人包:物件導向技能】課程。

***

前言

前一陣子Teddy與ezKanban團隊將ezKanban系統重構成能夠支援event sourcing,在重構的過程中,不免破壞了一些原本正常執行的測試案例。由於被影響的測試案例數量太多,雖然都是一些介面修改的小地方,但是數量一多還是很費工夫。

這個現象讓Teddy注意到原本的測試案例可讀性有點降低的趨勢,於是花了幾天重構測試案例。在這個過程中,Teddy發現雖然目前ezKanban系統一共有380個測試方法(test method),但還是有一些程式行為沒有涵蓋到。

另一方面,有些測試案例感覺又過於簡單,簡單到幾乎不會出錯,好像也沒什麼撰寫的必要,但是不寫又覺得心不安。例如,修改Board名稱的單元測試,最原始的程式碼就只有:

this.name = name;

簡單到不會出錯的程式就可以不用寫自動化測試,但是,因為採用領域驅動設計的方式開發ezKanban,所以除了this.name=name以外,還要發出BoardRenamed領域事件,這也增加了另一個需要測試它的動機。

因此,Teddy想到很多年前學過的Design By Contract(DBC;依合約設計),透過撰寫合約可以定義程式的行為,並在runtime自動驗證這些合約是否成立。若合約被違反,便代表程式有bug。合約撰寫越完整,可以大幅減少撰寫測試案例的數量。

這兩集Teddy要談在ezKanban中撰寫合約的方式,以及如何透過撰寫合約找到新的bug,之後再談如何簡化單元測試。

***

準備撰寫合約工具

合約有三種:precondition(前置條件)postcondition(後置條件)class invariant(類別不變量)。要在程式中撰寫合約需要程式語言支援。但可惜的是知名的商用程式語言除了Eiffel以外,Teddy就沒有聽過其他語言有內建支援合約。

退而求其次,如果有工具、框架支援,也是可以。但支援DBC的框架雖然有好幾個,但感覺都是實驗性質的成分比較多,而且很多都已經不再維護。

結論就是,離開Eiffel語言以外,先不要在其他程式語言追求完整的DBC支援。先求有,再求好。Teddy就是秉持這個精神,開始在ezKanban中撰寫合約。

一條合約就一個assertion,中文翻成斷言,也就是結果為真的敘述。講白了合約就是一個結果為true的if條件式。

例如,以下敘述都是assertion:

  • Teddy是泰迪軟體創辦人
  • 台灣中南部現在缺水
  • Pascal是Teddy收編的貓
  • 海水是鹹的
  • 你走路的速度小於光速


所以只需要把一個if條件式包在代表precondition與postcondition的method裡面,就可以用最陽春的方式撰寫合約,如圖1所示。

【圖1】土炮撰寫合約的Contract類別

***

合約範例1

【圖2】Board類別的rename method


先看一個簡單的範例:幫Board類別的rename method寫合約,如圖2所示。因為ezKanban套用了event sourcing,54~59行代表發出BoardRenamed領域事件,真正設定board name = newName 的程式碼在另一個私有方法handleBoardRename身上,如圖3所示。


【圖3】BoardRenamed領域事件的處理程式

圖3是rename方法的實作細節,訂合約的時候不用去理會他,把焦點放在圖2,幫rename撰寫合約:

precondition:

    requireNotNull("Board name cannot be null", newName);

postcondition:

ensure(format("Board name is '%s'", newName), getName().equals(newName));


這段合約表示:如果呼叫rename給定一個非null的字串newName(precondition的內容),則rename執行完畢後,Board name就會等於newName(postcondition的內容)。這也就是rename的語意

看到這裡Teddy要請問一個問題:「Board name可否為空字串?!

依據precondition,board name是可以接受空字串。如果你們的商業邏輯是規定board name不能是空的,就需要再加上一條precondition:

require("Board name cannot be empty", 
!newName.isEmpty());

***

合約範例2

最後看一個複雜一點的合約,Workflow類別的moveLane方法,如圖4所示。一個看板的Workflow(工作流程)可以有多個Lane,每個Lane代表一個工作步驟,它底下可以有多個sub lane。


【圖4】Workflow類別的moveLane方法

moveLane的:

precondition

  • lane id不能是null
  • new parent id不能是null
  • order是正整數
  • User id不能是null
  • 要移動的lane必須存在workflow裡面
  • 要移動的新位置(new parent)也必須存在workflow裡面
  • 不能移動到自己身上,或是自己的子節點

postcondition

  • lane的parent id等於new parent id(確定有移動到新的節點之下)
  • lane的order等於new order (移動後的順序是對的)
  • lane從原本的parent移除(確定lane已經不存在原本的parent,是移動而非複製)
  • 新parent的子節點都已排序
  • 舊parent的子節點都已排序

以上,如果合約訂得夠完整足以代表程式的行為,測試案例只要去呼叫這些method即可,不需要在測試案例裡面寫太多assertion,因為合約正確就代表程式行為正確

但前提是,你的合約要寫對。因為合約也是程式碼,也可能會寫錯XD。

***

友藏內心獨白:既使沒有程式語言的支援,合約也是一種很棒的設計方法。

2021年1月26日 星期二

領域驅動設計學習筆記(17):與外部系統的互動

Jan. 26 22:10~22:59

▲ezKanban使用者註冊範例


學員的問題

今天Teddy和ezKanban團隊mobbing的時候,有一位去年上【領域驅動設計與簡潔架構入門實作班】的學員突然傳訊息給Teddy。一開始對方說要詢問anti-corruption layer實作的問題,但依據Teddy多年的經驗,當有人覺得他的問題是A的時候,通常他的問題都不是A,而是X或Y或Z

討論到後來對方給了Teddy一張從網路抓下來的圖如下:


▲圖1:學員用來詢問Teddy的例子,擷取自網路


對方將付款服務定義一個介面,放在application service layer(也就是clean architecture的use case layer)。他的問題是:

  • 當付款成功之後,是否由application service 發出Paid Successfully領域事件?
  • 如果是,但DDD不是提到領域事件是由Aggregate Root所發出,從application service發出領域事件OK嗎?

***

區分不同的視角

Teddy記得Martin Fowler在《UML Distilled》提到,一個class diagram有三個不同的視角(perspective):

  • Analysis
  • Design
  • Implementation

如果讀者把analysis class diagram當成implementation class diagram來解讀,一定會覺得圖中少了很多細節,甚至會誤解這張圖。

同樣道理,這位學員從網路上看到的event storming例子,頂多就是process level的圖,還沒到design level,所以直接用這張圖來推論實作,就可能會造成誤解。Teddy從design level的視角,將圖1重新繪製如圖2。


▲圖2:將圖1加上Aggregate,進入design level event storming


圖2有兩個Aggregate,因為還不知道要叫什麼名子,先用AA和BB代表。圖2的主流程如下:

  1. 使用者執行付款(Pay,藍色便利貼)命令
  2. 該命令送給AA(黃色便利貼)
  3. AA與外部系統(粉紅色便利貼,此範例的外部付款系統為PayPal)溝通付款細節
  4. 如果付款成功則產生Paid Successfully領域事件(橘色便利貼)
  5. 該領域事件引發一條規則(紫色便利貼)—寄送付款成功的信件給使用者
  6. 因而又觸發了寄送Email(Send Email)命令
  7. 以此類推

根據以上分析,很顯然付款服務應該是一個domain service而非application service。AAA呼叫付款服務這個domain service,並依據付款的成功與否送出不同的領域事件。如此一來便回答了學員的問題。

***

友藏內心獨白:視角清楚,思慮也跟著清楚,答案就在不遠處。

2021年1月22日 星期五

愛上Mob Programming(下)

Jan. 21 18:22~19:50

▲喵星人也要一起mobbing


前言

乍看之下,mobbing是一種浪費,整個團隊一起開發卻只用一台電腦寫程式,感覺是把原本可以平行同時工作變成只能循序工作。今天Teddy從敏捷與精實開發的角度來看,為什麼值得做mobbing。

首先,所有的開發工作,不管是多少人一起開發,大致可以分成兩種工作模式:

  • Component Development:開發人員專注於開發軟體系統的某個元件,例如,寫後端領域邏輯、設計資料庫schema與下SQL、寫前端JavaScript、寫CSS。這種模式底下開發人員所完成的工作並不能直接使用,需要經過另一個人整合之後,例如串起前後端,才會變成一個完整的功能。傳統的瀑布式開發,大多採用這種專業分工的模式。
  • End-to-End Development:又稱為feature development,開發人員需要負責整個功能,也就是所謂的全端開發。全端開發工作並不限定一個人的全端,也可以是交由一個團隊來完成。例如Scrum團隊就是一個可以交付功能的跨職能團隊(全端團隊)。

***

單人開發

單人開發是一般人最熟悉的開發模式,又稱為Solo Programming。如果是採用component development的單人開發,所完成的工作並不能直接交付給使用者,也就是無法直接產生價值,還需要靠整合流程將所有的元件合併成可使用的功能。

從精實開發的角度來看,這種模式所完成的工作都是半成品(work in process/progress;WIP),做得越多,只是累積更多庫存,徒增浪費

如果是feature development的單人開發,這個人就是所謂一條龍全端工程師,可以獨立產生價值。好的全端工程師非常難得,可以做到一個人的end-to-end。但人的能力總有侷限,有些全端工程師是屬於比較偏後端的全端工程師,有些則是前端比較強的全端工程師,也有那種前後端都不熟的全端工程師XD。

在這種情況下,全端工程師的產出,就受限於他自己最弱的一環。假設Teddy的後端能力有100分,前端能力只有20分,Teddy的總產能,不是 (100 + 20) / 2 = 60分,而是只有20分。這就是瓶頸,流程中生產力最弱的那個環節將會決定整個系統的輸出

此外,單人開發還會造成維護的問題。如果這個人離職了,他所負責的程式通常連他自己都看不太懂了,更何況要找一個人來交接工作,難上加難,錯誤百出。

***

雙人編程

為了解決單人開發的問題,eXtreme Programming(XP)很早就鼓吹雙人編程或稱為結隊編程的pair programming。兩個人一起開發,一位扮演司機,另一位扮演領航員,兩人彼此互補,兩顆腦袋一起使用,可以減緩單人開發所遭遇到瓶頸的問題。假設採用feature development:

  • 小明:後端能力90分,前端20分,系統產出20分。
  • 小英:後端能力40分,前端60分,系統產出40分。

兩個人一起合作,整個系統的產出是60分。看到這邊你可能會覺得Teddy在莊孝維。小明、小英個別開發,20 + 40 也是 60分啊,而且搞不好個別開發速度更快。

但實際上,個別開發的速度,除了受限於每個人自身的能力,還受到很多因素影響。例如,專注力、解決bug的能力、當下看出設計問題的能力等等。一個人開發經常會陷入自己的盲點,導致被一些很簡單的「低級bug」給困住跑不出來,徒增很多試錯的時間。

雙人編程可以減少上述問題的發生,又可以自然產生一種良好的「備胎效益」—所有的程式碼至少有兩個人以上都有能力開發與維護,萬一人員異動也不至於整個停擺。

但雙人編程也會造成其他問題,例如如何搭配成員。把兩個菜鳥搭配在一起顯然無法減少瓶頸所造成的限制生產力問題,把兩個高手高高手安排在一起,可能大部分的時間都在吵架「華山論劍」,樂於忙著練功卻忘了實際的開發進度。

***

團隊編程

在mob programming的情況下,團隊成員每個人同時間都貢獻他自己最強的能力在同一件事情上面。例如團隊有五個人:

  • 小明:UI/UX擔當。
  • 小華:JavaScript高手。
  • 小英:後端專家。
  • 小龍:新人。
  • 小陳:Product Owner(PO)。

在mobbing的情況下,這五個人一起開發。假設現在要開發ezKanban的使用者註冊功能,如下圖。


▲ezKanban使用者註冊流程


如果是跑Scrum,PO需要先寫好user story,然後在sprint planning meeting帶過來跟團隊解釋。此時團隊可能選擇估算每一個user story的大小,接著再將user story切成tasks。

Spring planning結束後,產出spring backlog並將其布置成task board。然後每天Daily Scrum的時候團隊在task board前面報告三件事並認領新的工作。Sprint結束後還有review與retrospective活動,

如果改用mobbing,因為整個團隊一起開發,所以也就不需要另外再開會。要開發註冊功能,就直接從event storming的圖中選出Register User這個使用案例,如果開發人員對需求有問題,例如:

  • 要如何註冊?在ezScrum保存一份使用者註冊資料,還是結合Facebook、Google、Line的帳號即可?
  • 要不要整合LDAP做到企業內部單一登入?
  • 註冊畫面需要填寫那些資料?密碼的長度有限制嗎?

以往這些問題可能會在spring planning討論,如果沒有討論到,例如要不要支援LDAP,等到實作的時候,開發團隊就必須要再去詢問PO。但PO不一定可以立即被找到,因此就產生所謂等待浪費。也有可能開發人員自作主張,覺得應該要做到很有彈性,所以在沒有詢問PO的情況下自己加入支援LDAP的功能,導致過度設計,等review的時候才發現這個問題。過度設計,或過度生產,也是一種浪費。

在mobbing的情況下,有任何需求面的問題,當下就可以和PO討論,減少大量部必要的浪費。

在開發整個註冊功能的end-to-end過程中,有時候處理UI/UX、有時候處理後端、有時候處理前端、有時候釐清需求,團隊中每一個對該領域最擅長的人都可以貢獻自己全部的力量。當遇到自己不熟的領域,也可以從別人身上學習,逐步加強自己的能力。

至於新人,加入團隊當下就可以有貢獻。因為mobbing的時候大家輪流當司機(拿鍵盤與滑鼠的那個仁),就算自己不知道怎麼寫,導航員也會告訴你怎麼做。這種在工作中學習的模式,可以達到最快上手的目的。

***

單件流

Mobbing可以做到精實開發提到理想工廠單件流的效果,在生產線上,每個節拍時間就有一個工作在每個工作站中流動。不需要庫存,也沒有浪費(假設沒有缺陷XD)。Mobbing與生產線單件流的差別在於,mobbing是整個團隊跟著工作跑,當工作處於開發後端階段,整個團隊就開發後端;處於開發前端階段,就一起開發前端,只有一件工作在每個工作站流動。

Teddy以一個會寫程式的PO的角色,和ezKanban團隊mobbing近七個月,這段時間沒開過以往Scrum裡面的任何會議,但卻比以往任何時間更了解產品開發的進度與品質。

開發軟體,應該和打籃球、打橄欖球一樣,整個團隊同一時間一起在同一個球場打球。現在想一想,這不是再自然也不過的事嘛!

***

友藏內心獨白:用過就知道,說破嘴也沒用。

2021年1月21日 星期四

愛上Mob Programming(上)

Jan. 21 01:08~14:24

▲ezKanban團隊mobbing實況


緣起

約略四年多前Teddy聽指導教授鄭老師提到實驗室開始全面採用mob programming(簡稱mobbing,這是Teddy第一次聽到這個名詞。

如果鄉民們把mob programming丟到Google翻譯,會得到暴民編程的中文解釋,因為mob的英文就是暴民的意思。最近很流行的新聞—美國擁護川普的群眾衝入國會大廈,英語新聞就用mob來指稱這群人。

***

當然mob programming絕對不會是找一群暴民來寫程式,根據維基百科的解釋:

(譯自英文) Mob編程是一種軟件開發方法,其中整個團隊在同一時間,同一空間和同一台計算機上處理同一件事。這類似於結對編程,在結對編程中,兩個人坐在同一台計算機上,並同時在同一代碼上進行協作。通過mob編程,可以將協作擴展到團隊中的每個人,同時仍然使用一台計算機來編寫代碼並將其輸入到代碼庫中。

也就是整個團隊一起用一台電腦同時開發軟體。

Teddy第一次聽到這種作法,腦中的第一個反應是出現黑人問號的畫面。Pair programming很合理,但整個團隊綁在一起用一台電腦開發,真的假的?!

***

初體驗

兩年多前Teddy幫忙帶幾個實驗室的研究生開發看板軟體—ezKanban,採用類似Scrum的方式開發,兩個禮拜Teddy和學生review一次進度,每次review會跟學生討論設計細節,包含design review與code review。雖然Teddy覺得自己已經把話講得很清楚,但常常在下次review的時候才發現學生的實作並沒有真的達到Teddy的要求,所以需要再解釋一次。長期以往雖然還是有迭代與增量,但整個ezKanban的開發進度總是不如Teddy的預期。

約略一年多前有一次機會,Teddy在實驗室跟團隊一起mobbing,那次的體驗並不好。首先,實驗室的空間並不大,Teddy這個人懶散慣了,不太習慣幾人擠在一個沒有方便伸展與走動的空間。其次,Teddy年紀大了,雖然實驗室mob環境有一台65吋大螢幕,但Teddy看著這個螢幕還是有點不太習慣。寫到這裡Teddy才突然想到,不一定是螢幕的問題,也有可能是實驗室椅子太難坐的關係。畢竟平常在家裡工作,Teddy坐的都是Herman Miller Aeron的高級人體工學椅XD。

總之Teddy的第一次mobbing經驗並沒有激起到自己繼續跟學生一起mobbing的想法。


▲要價不菲的人體工學椅

***

疫情的正面影響

為了Clean Architecture與領域驅動設計(Domain-Driven Design;DDD)課程的教學需要,Teddy自己也開發一套看板系統—cleanKanban,但因為Teddy前端不熟,所以cleanKanban一直就只有後端。

去年七月初暑假的時候,Teddy覺得這樣下去也不是辦法,ezKanban團隊開發一套看板系統,Teddy自己也開發一套。ezKanban團隊的前端做得還可以,但後端還差很多。Teddy的cleanKanban剛好相反,後端好棒棒,但前端……醒醒吧,你根本沒前端啊!

因為新冠肺炎疫情的關係,人們開始廣泛接受以線上活動取代實體活動。Teddy也受到這個正面的影響,去年七月暑假剛開始,Teddy決定把cleanKanban合併到ezKanban裡面,並且跟ezKanban團隊一起mobbing。但Teddy採用skype遠端連線的方式,ezKanban團隊在實驗室mobbing,並分享桌面給Teddy。如此一來,Teddy不用出門,可以舒舒服服坐在家裡的電腦前面玩貓兼用嘴吧寫程式

有此工作環境,夫復何求!

▲Teddy在家裡遠端參與ezKanban團隊的mobbing實況


***

全新的體驗

換個合作模式之後,去年暑假ezKanban進度大爆發。Teddy覺很棒,於是暑假過後繼續跟學生約定每周mobbing的時間。

▲上學期ezKanban團隊mobbing時間表


從去年至今Teddy已經連續和ezKanban團隊mobbing了近七個月,每周平均約20小時,累積mobbing時數應該已超過500小時。

下一集Teddy從敏捷與精實開發的角度來細談為什麼mobbing是一種好的開發模式。

***

友藏內心獨白:看似浪費實則精實。

2021年1月19日 星期二

領域驅動設計學習筆記(16):領域模型要放什麼東西?

Jan. 19 10:00~11:07


客人的問題

上周末在【領域驅動設計與簡潔架構入門實作班】有學員問Teddy…

學員:DDD的building blocks包含Entity、Value Object、Aggregate、Service、Repository、Factory等,為什麼你的domain model裡面沒有畫出Repository?

Teddy:為什麼要在domain model畫Repository?

學員:因為我們要「派工」啊。

Teddy:派工 ?!

學員:對啊,我們要畫設計圖,然後派工給工程師去寫程式。

***

領域模型作為瞭解問題的工具

領域驅動設計是透過「建立領域模型」來驅動整個軟體開發設計的方法。模型屬於solution domain,如果採用最常見的物件導向分析設計方法,領域模型的主角就是代表問題領域裡面重要概念的「物件」。

例如,開發看板系統,在「看板系統」這個問題領域中,重要的概念包含Kanban Board、Workflow、Stage、Swimlane、Kanban Card、WIP Limit、Flow、Lead Time等,它們自然成為領域模型的當然候選人。

你在跟看板的領域專家討論問題時,會出現Repository或Factory這種概念嗎?應該不會吧!所以,你不會在領域模型看到它們。

***

但是我要派工啊

好、好、好,Teddy知道你要派工。領域模型首先作為了解問題領域的分析工具,最後當然還是要用程式碼實作出來。傳統OOAD透過「模型轉換」過程,將分析模型轉成設計模型再轉成實作模型。但DDD希望針對同一個bounded context,整個開發過程就只有一個模型,用這個模型作為領域專家與團隊的通用語言(ubiquitous language),最後再將通用語言用程式碼實作出來,達到ubiquitous language in code的境界。

想要將DDD的領域模型再轉成詳盡的設計模型然後拿給程式設計師去寫程式(完成一個派工的動作XD),是waterfall的做法。如果號稱使用DDD但還是採用這種做法,代表團隊根本沒有共通語言,所以才需要使用另一種語言(派工文件)來溝通

看到領域模型中的Aggregate,自然就知道需要搭配一個Repository來負擔持久化(把Aggregate狀態存到資料庫中)的責任,根本不需要在領域模型裡面表達這種關係。

***

DDD為什麼流行?

DDD藍皮書2004年出版至今也過了10幾年,為什麼近幾年才流行?Teddy認為除了很多人想透過DDD來開發微服務系統以外,另一個原因就是DDD、Event Storming與Clean Architecture、TDD這些做法全部加起來,非常適合用來作為落實敏捷開發的一種實踐方法,符合目前整個軟體開發的主流做法,做得好可以讓你的軟體變軟。

軟體變軟,才能夠支撐公司業務變得更敏捷(靈活)。DDD是domain-driven design,不要搞成document-driven design。

***

友藏內心獨白:軟體開發的特性就是「按圖施工,保證不成功。」

2021年1月10日 星期日

領域驅動設計學習筆記(15):拿掉領域事件所造成的跨Bounded Context原始碼依賴關係

Jan. 10 22:29~23:59

▲圖1:ezKanban三個bounded contexts之間訊息流動方向


領域事件造成的原始碼相依性

上一集〈領域驅動設計學習筆記(14):依據領域事件流動方向調整Bounded Context的依賴關係〉提到依據領域事件流動方向將ezKanban的bounded context依賴方向由原本的:

  • Team—>Account
  • Team—>Board

調整成如圖1所示的:

  • Team—>Account
  • Board—>Team


請參考圖2,以UserRegistered領域事件為例,當使用者成功註冊之後,系統會自動幫該使用者建立一個預設的Team(團隊),當使用者登入系統之後,就可以在這個預設的團隊裡面新增Board(看板)或Project(專案)。

▲圖2:UserRegistered領域事件由Account bounded context傳遞到Team bounded context

ezKanban雖然切成三個bounded context,但目前採用Monolithic(單體)佈署方式,因此三個bounded context透過一個共享的in memory event bus來發送與接收領域事件。圖3展示位於Team bounded context的event handler—NotifyTeam程式,它監聽UserRegistered領域事件,當該事件發生時,呼叫CreateTeamUseCase幫剛註冊的使用者新增一個預設的團隊。


▲圖3:NotifyTeam事件處理程式

看到這裡鄉民們應該就很清楚,基本上Account與Team是兩個獨立的bounded context,彼此之間只有在runtime(程式執行期間)透過領域事件產生相依,compile time(編譯期間)原則上是不需要產生原始碼相依性。但因為一開始Teddy與ezKanban團隊偷懶的關係,讓Team直接去聽Account的UserRegistered領域事件,因此Team需要將UserRegistered類別import進來,所以產生Team對Account bounded context的原始碼依賴。

***

拿掉原始碼依賴

拿掉Team對Account bounded context的原始碼依賴也不會很麻煩:

  1. 設計一個RemoteDomainEvent類別,用來代表跨bounded context的領域事件,並將它放在所有專案都會參考的共用模組裡面。
  2. 不要直接把UserRegistered類別傳給Team,而是把UserRegistered「序列化」(例如轉成JSON格式)之後用RemoteDomainEvent將它包起來再傳出。
  3. Account不再監聽UserRegistered領域事件,改監聽RemoteDomainEvent。當收到RemoteDomainEvent之後,將其內容打開判斷是否為UserRegistered領域事件,如果是再執行原本的事件處理程序。


請參考圖4,NotifyAccount是位於Account bounded context的事件處理程式,它監聽自己的UserRegistered領域事件,並將其轉成RemoteDomainEvent。


▲圖4:NotifyAccount事件處理程式


圖5展示新的NotifyTeam事件處理程式,它改成監聽RemoteDomainEvent,並依據事件的tag與事件名稱將其轉交給合適的事件處理程式來處理。


▲圖5:新的NotifyTeam事件處理程式


將原始碼相依性徹底拿掉,從佈署的角度來看,要從Monolithic(單體)轉成Microservices(微服務)對系統架構來講要改動的幅度也就比較小,增加佈署方式的選擇彈性。

***

依賴依然存在

最後提醒一點,Account與Team這兩個bounded context,從Context Map的角度來看,應屬於Customer/Supplier的上下游關係,所以兩者之間還是有依賴關係,只不過這個依賴關係由原始碼依賴改成符合真實世界狀況的執行期間的訊息依賴。如果Account的UserRegistered領域事件介面改變,還是有可能會導致Team發生執行期間錯誤(runtime exception)。

***

友藏內心獨白:動態比較有彈性也比較容易出錯XD。

2021年1月8日 星期五

看懂一本書

Jan. 08 16:47~17:58


看完一本書

Teddy在北科上課的時候,經常會告訴學生利用研究所求學期間培養一種學習體驗:「把一本專業書籍從頭到尾讀完、讀懂」

對軟體開發這種知識工作者而言,學習速度就是軟體開發的瓶頸。如果在求學階段就具備「看懂一本書」的能力,畢業之後面對各種新知,學習起來也就不會覺得特別困難。即使覺得困難,也不會覺得是一件不可能的任務,因為你曾經達成過這種任務,體驗過看懂一本書的Quality Without A Name。

***

打賭

去年秋天開學的時候,Teddy問修「敏捷與精實軟體開發」課程的同學有沒有人曾經把一本專業書從頭到尾讀完的經驗?班上只有一位同學舉手。Teddy問他,你讀完的是什麼書?他說:「Clean Architecture」。

Teddy跟這位學生說:「我問你一個Clean Architecture的問題,如果你答對,這門課就直接及格,可以不用來上課。如果答錯,就退選,好不好?」

同學拒絕了。

Teddy想要測驗一下這位同學對於自己「看完一本書」的信心指數,因為Teddy所謂的「看完一本書」,是指「把這本書看懂」,不是表面上把書「看完(翻完)」而已。

***

怎麼算是把書看懂?

「看懂一本書」是一個很抽象的概念,而且看懂的層次也會隨著讀書的次數與自身體驗的增長而有所不同。Teddy有幾個自己土法煉鋼的方法:

  • Teddy在「增進學習力的三個練習」提到,可以透過了解專業名詞定義、有能力用一般人可以聽懂的比喻來解釋專業術語、看到解決方案之後,可以反推論出這個解決方案所要處理的問題是什麼(problem before solution,問題先於解決方案)這三個方法來讀書。
  • 自問自答,問自己為什麼。例如,學習Design Patterns的時候,學到Adapter設計模式,可以問自己:為什麼要用Adapter?Adapter要解決什麼問題?沒有Adapter之前的世界長什麼樣子?遇到相同問題,除了Adapter以外,還有哪些的解決方案?是什麼因素導致你選擇Adapter而不選擇其他解決方法?每問一個問題,就等於幫自己的知識累積一個層次,問題問多了,也就很難遇到可以把你問倒的人。
  • 實踐。書上很多知識,如果只是透過「腦袋中的理解力」去學習,哪只是學了一半,另一半落實(實作)能力還是要靠自己動手。建築師Alexander說:「A pattern is a process and a thing」從智力上去理解一件事,只是知道這個Thing,要動手去做,才能了解Process。特別是對於軟體開發人員而言,「知道」設計模式、單元測試、CI/CD、TDD、DDD、Clean Architecture、CQRS、微服務,但卻沒有動手做過,這樣的知道,不能算是真的知道。
  • 多看幾遍 (迭代)。好書,不是一次就可以讀完、讀懂。所以,多看幾遍,隨著自己的生活體驗不同,每次都可以看到不同的東西。

***

再續前緣

昨天Teddy在北科的課程期末考,沒想這位剛開學時說過讀完Clean Architecture的這位同學念念不忘這件事,希望Teddy下禮拜學期最後一週可以問他一個Clean Architecture的問題,他想挑戰自己的學習成果。

就如他所願,下禮拜問他一個Clean Architecture的問題。鄉民們可以預測一下,同學會不會答對呢?下周在「搞笑談軟工FB社團」公布對決結果。

***

友藏內心獨白:沒賭注就不刺激了啊XD。

2021年1月5日 星期二

領域驅動設計學習筆記(14):依據領域事件流動方向調整Bounded Context的依賴關係

Jan. 05 13:45~17:38

▲事件流動方向也代表bounded context之間依賴的方向


今天談一個開發ezKanban時,只專注在core domain而沒有同時關照到context map所踩到的坑 。


背景

ezKanban目前有四個Bounded Contexts:

  • Account:負責註冊。
  • Team:負責管理團隊、專案、以及看板的使用權限。
  • Board:負責表達看板中的工作流(Workflow)以及卡片(Card),此為ezKanban的核心領域(Core Domain)。
  • Report:負責產生累積流量圖(Cumulative Flow Diagram;CFD)、交期分布圖(Lead Time Distribution Chart)、控制圖(Control Chart)等圖表。


這幾天ezKanban團隊正在開發Add Member To TeamAdd Member To Board的使用案例,原本的設計Add Member To Team屬於Team Bounded Context,而Add Member To Board則是屬於Board Bounded Context的功能,如圖1所示。

▲圖1:Team與Board Bounded Contexts的domain model(部分)

***

問題

Add Member To Team做完之後緊接著要做Add Member To Board,此時發現:

  • Board member必須是team member,因此Add Member To Team需要接受前端傳來的List<Team Member>當作參數。
  • Team bounded context的Team member被移除之後,很顯然地Board bounded context的board member也要被移除。
  • 因為Team與Board屬於不同Bounded Context的不同Aggregate,因此彼此之間的狀態同步要透過領域事件達到最終狀態一致性。

到目前為止看起來都沒什麼問題,問題出在:

  • Team會去聽Create Board所產生的BoardCreated領域事件,以便在它身上加上一個Board物件,作為在團隊儀表板功能顯示給使用者看。
  • Board也要聽TeamMemberRemoved領域事件,以便將自己身上的Board member移除。

如此一來,Team和Board這兩個bounded context便產生循環參考

***

真理之源 (Source of Truth)

Team和Board因為領域事件產生耦合,在分散式系統中,可以在它們兩個之間架上Message Queue,以隔開兩者的直接相互參考。這樣做程式可以動沒問題,但這個現象讓Teddy思考:「兩個Bounded Context互相參考,是否意味著Board bounded context裡面的board物件,應該移到Team bounded context裡面?」

ezKanban的開發順序,先從Board bounded context這個core domain開始,此時主要關注點是工作流程是否可以表達問題領域的各種狀況,以及卡片的設計與流動,等於乎應看板的三個核心原則:

  • 視覺化
  • 限定WIP
  • 管理工作流

所以一開始代表「看板」概念的領域物件—Board被設計放在Board bounded context,等到board bounded context基本功能完成之後,團隊才開始實做Team bounded context,一直到實作至Add Member To Board才發現上述相互參考的問題。

Board這個概念,原本在Team bounded context與Board bounded context都存在,這沒問題。ezKanban的問題在於:「新增Board的時候,這個代表Board資料的Source of Truth應該要存在Team bounded context,而不是存在Board bounded context。」為什麼?現在想想也很簡單,因為從使用者操作ezKanban的時間順序來看,使用者是先在Team bounded context新增一個Board,接著才進入這個Board去設計工作流程與卡片,如圖2。


▲圖2:Team bounded context設計好之後,很明顯可以看出,Board的原始資料應該存放在Team裡面

***

友藏內心獨白:訊息流動的方向也要注意。