l

2021年10月5日 星期二

頭痛醫腳,腳痛醫頭

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。有沒有可能是設計過於複雜導致錯誤?如果是,應該先改善設計品質,讓設計簡單到不會出現明顯的錯誤。

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

***

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

2021年9月15日 星期三

成本與價值

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

***

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

***

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

2021年9月14日 星期二

有沒有發出聲音?

September 14 10:25~11:16

▲電影《達摩》畫面


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

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

***

梁武帝

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

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

達摩:「無功無德。」

***

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

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

***

政客

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

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

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

***

什麼是真的?

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

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

你想要成為怎樣的樹?

***

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

2021年9月13日 星期一

Clean Architecture是一種過度設計嗎?

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。

2021年9月10日 星期五

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

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。

2021年9月9日 星期四

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

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身上就光溜溜沒有行為,變成Martin 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)的角度,再談貧血模型與充血模型的不同觀點。

***

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

2021年9月8日 星期三

【還少一本書】Get Your Hands Dirty on Clean Architecture

September 08 20:30~23:34


前言

目前市面上Clean Architecture的書,跟日本進口壓縮機一樣,非常稀少。Teddy在2017年介紹過〈[還少一本書] Clean Architecture〉,事隔多年後,今天要介紹剛讀完的另外一本《Get Your Hands Dirty on Clean Architecture》。

這本書只有薄薄的137頁,書中探討實作Clean Architecture遇到的程式實作問題。Uncle Bob寫的《Clean Architecture: A Craftsman's Guide to Software Structure and Design》雖然多達400頁,但書中的敘述比較抽象,實作的程式碼趨近於0。導致許多人讀完《Clean Architecture》之後,在程式實作的時候形成「一個架構,各自表述」的窘境。而「在乾淨的架構上弄髒你的手」這本書,試圖彌補Uncle Bob留下的實作缺口。

接下來Teddy逐一介紹這本書的優點以及可改善之處。

***

優點--單一責任的使用案例類別

這本書的前兩章談傳統階層架構所造成的問題,以及如何透過依賴反轉來解決這些問題。作者在前兩章還沒提到程式實作,而這兩章的大部分內容在Uncle Bob的書中已經說得很清楚。Teddy覺得比較值得注意的一點是,書中第15頁提到:

The use cases are what we have called services earlier but are more fine-grained to have a single responsibility, thus avoiding the problem of broad services that we discussed earlier. (使用案例就是我們之前所說的服務,但更細粒度地具有單一職責,從而避免了我們之前討論的廣泛服務的問題。)

Clean Architecture的使用案例,有兩種常見的實作方式。第一種就是上述所說的broad service,一個service物件身上包含好多方法,每一個方法代表一個使用案例。DDD紅皮書《Implementing Domain-Driven Design》的書中範例就是典型的broad service寫法。

另一種方式就是將使用案例提升為類別,一個使用案例類別只會做一件事,也就是上述符合單一責任原則的fine-grained service。

Teddy自己是採用一個類別代表一個使用案例(fine-grained service)的做法。

***

優點--程式結構

在Uncle Bob書中由Simon Brown所寫的第34章The Missing Chapter,談四種程式碼結構的方式:package by layer、package by feature、ports and adapters、package by component。雖然這個章節畫了好幾張圖來解釋這四種方法,但Teddy覺得書中的描述還是少了一些重要的細節,導致在實作上有太多的可能性。

這個問題其實很容易釐清,講那麼多還不如給一張圖畫出程式目錄結構就搞定了。

……為什麼不給code哩?

而本書第3章則是以實際的程式結構範例重點說明package by layer與package by feature的優缺點,最後給出作者的建議方式:先package by feature再package by layer。

雖然作者沒有針對Uncle Bob原本書中的四種目錄結構方式詳細比較,但至少給了一個還算是非常具體的實作建議。

***

優點--具體的使用案例實作程式範例

本書第4章談使用案例的實作,以實際的程式碼對應到Uncle Bob書中所說的use case input、use case與use case output。本書把use case input稱為input model,並將其包裝成Command物件(可參考書中第37頁SendMoneyCommand程式碼)。

將input包裝成Command是很常見的作法,但這一點與Teddy的慣用做法不同,Teddy使用UseCaseInput類別來實作input。

Teddy在實作Clean Architecture的時候同時套用DDD與Event Storming,在Event Storming中藍色便利貼代表Command,也就是使用者對系統下的命令。有人將Event Storming的Command當成use case input,Teddy則是直接將Command用使用案例實作,而執行該Command所需的輸入參數當成use case input。所以Teddy把Command這個概念保留給使用案例,而不是使用案例的輸入參數。

儘管書中的實作方式與Teddy慣用方式有所差異,但概念上還是一致。

***

本章另外還有四個亮點:

  • 輸入參數驗證與商業邏輯驗證的差異,以及應該在哪個軟體架構階層中去做驗證。
  • 貧血模型(anemic model)和豐富模型(rich model)對於使用案例的實作有何影響?
  • 使用案例的輸出
  • 唯讀的使用案例實作

***

優點—跨層的物件映射(Mapping)

Teddy讀完Uncle Bob的《Clean Architecture》之後將其重點歸納為三個原則,細節請參考〈再談Clean Architecture三原則〉。其中有一個原則叫做〈跨層原則〉,書中關於此原則的實作著墨甚少,只提到當物件跨層傳遞的時候,會使用Mapper設計模式將物件映射成另一種型別。

實務上,也有不少開發人員反對跨層映射,覺得太麻煩,不但沒效益還產生許多類似的重複程式碼。

本書第8章詳細說明No Mapping、One-Way Mapping、Two-Way Mapping與Full Mapping(《Clean Architecture》書中建議的做法)這四種做法的優缺點。作者建議對於web controller layer採用full mapping,但在persistence layer就不建議使用,因為作者覺得開銷太大,不划算。

但這一點Teddy並不同意作者的看法,在ezKanban中,不管是web controller layer還是persistence layer,都採用Full Mapping。雖然一開始需要花時間寫mapper程式,但在後來Teddy不斷重構entity layer的過程中幫了極大的忙。不管entity layer實作如何修改,persistence layer與web controller layer幾乎完全不受到影響。

***

優點—Main Component的實作

Clean Architecture》第26章The Main Component也是有點抽象的一章,書中提到Main Component是「最髒」的元件,而且每一種系統配置都需要用一個Main Component來代表,在實作上有不少讀者不知如何具體落實這個Main Component。

Get Your Hands Dirty on Clean Architecture》第9章則是以具體的程式碼說明Main Component的實作,又彌補了一個《Clean Architecture》書中的一個實作缺口。

***

可以更好

提了這麼多優點,最後談談可以做得更好的地方。

第一點雖然Teddy把它列為缺點,但也可以說是優點。因為這本書很薄,雖然可以很快一窺Clean Architecture的實作面貌,但對於完全沒有學過Clean Architecture的鄉民而言,如果要光靠這本書就「徹底學會」Clean Architecture,恐怕有點難度。Teddy建議搭配Uncle Bob的《Clean Architecture》一起服用,一本學理論,另一本學實作,學習效果更佳。

第二點Teddy覺得比較嚴重,書的標題雖然是「Get Your Hands Dirty on Clean Architecture」,但作者在實作上滿多地方參考六角形架構(Hexagonal Architecture)並使用六角形架構的術語。例如,在第三章程式結構中使用inoutport這些六角形架構的術語,而非Clean Architecture書中採用的術語。雖說Clean Architecture與Hexagonal Architecture大同小異,但兩者所用的術語還是略有不同。所以在一本標榜Clean Architecture實作的書中採用「鄰國術語」,Teddy還是覺得略有不妥。

此外,硬是要用in、out來區分adapter,Teddy也是略有疑慮。作者提到in adapter代表由外部呼叫系統,out adapter代表有系統呼叫外部。例如,web controller是in adapter,repository或persistence adapter是out adapter,這沒問題。那麼Event Bus呢?它有時候是in,有時候是out,要把它放在那個目錄裡面?還是把接收資料的event bus adapter放在in,傳送資料的event bus adapter放在out?Teddy沒有這樣設計過,所以對於這種區分adapter的方法還需要思考一下。

第三點,書中第6章:Implementing a Persistence Adapter的實作建議Teddy不太認同。書中建議在這裡也套用單一責任原則,讓一個Persistence Adapter就只做一件事,例如將Account物件的載入、更新、建立設計成以下三個介面:LoadAccountPort、UpdateAccountStatePort、CreateAccountPort。Teddy比較建議在這裡套用DDD的Repository設計模式,讓一個Aggregate對應一個Repository來處理儲存的問題。

以上述的Account為例,若採用event sourcing來保存物件狀態,根本不會有書中建議的UpdateAccountStatePort與CreateAccountPort介面,只需要save與load這兩個介面。

最後一點,這是一本介紹Clean Architecture實作的書,程式範例理應很乾淨。但書中有部分範例卻「不太乾淨」,例如下圖書中第35頁的SendMoneyService,它應該位於Clean Architecture的Use Case層。直接在它身上貼上@Transactional似乎有跟框架產生耦合的嫌疑。

***

友藏內心獨白:值得一讀。

2021年9月5日 星期日

領域驅動設計學習筆記(20):再談Aggregate Root實作

September 05 20:00~21:30


前言

Teddy之前曾用三集談過Aggregate的基本實作注意事項:

後來在今年學了DCI之後,又從DCI的觀點將Aggregate重構一波:

今天要談的問題,在〈領域驅動設計學習筆記(6):Aggregate (中)〉曾經討論過,就是Aggregate Root應該回傳唯讀的內部Entity給客戶端。原本Teddy採用reflection-util這個工具自動產生immutable proxy。reflection-util需要使用JDK 8以及之後的版本,後來Teddy改用JDK 16之後,發現reflection-util不支援Record類別。

Record類別本身就是不可修改,其實不需要幫它產生immutable proxy。但reflection-util以為Record是一般的類別,會試圖幫它產生一個Proxy(透過子類別的方式)。因為Record類別是final,無法產生子類別,所以reflection-util就破功了。

***

替代方案1: 修改 reflection-util原始碼

▲ImmutableProxy類別透過isImmutable方法判斷哪些型別本身就是不可修改的型別


看了reflection-util的原始碼,其中ImmutableProxy類別的isImmutable用來判斷一個物件是否為Immutable,如果是就不需要幫它產生一個Immutable Proxy。由於ImmutableProxy類別本身是final,而且isImmutable又是static,因此只能修改原始碼,把Record加入判斷中,如此一來就可以在Java 16中繼續使用reflection-util

解決了不支援Record的主要問題,又發現reflection-util不認得Teddy自己設計的final類別,如下圖所示:


reflection-util遇到不認識的final類別也會出錯,無法產生Immutable Proxy。

Teddy認為比較好的方式是ImmutableProxy可以接受從外部注入一個isImmutable方法,如此一來便可動態決定哪些類別原本就是Immutable,不需要再幫它們產生Immutable Proxy。

經過一番嘗試之後,雖然可以修改reflection-util,但Teddy不想自己維護一份原始碼,再加上套用Clean Architecture的情況下盡量不要在Entity Layer使用外部函式庫,所以就決定放棄這個方法。

***

替代方案2: 老師傅手工打造Immutable Proxy

reflection-util原始想法是可以幫任意物件產生一個Immutable Proxy,雖然很方便但因為功能很通用所以遇到的問題(挑戰)也比較多。Teddy本來想偷懶直接使用reflection-util,後來念頭一轉想說算了,自己動手幫Aggregate Root打造合適的Immutable Proxy。

先幫鄉民回憶一下ezKanban的領域模型,下圖是Workflow Aggregate類別圖,其中Workflow是Aggregate Root,其他類別是Entity。

▲Workflow aggregate類別圖


如下列所示,Workflow有幾個方法會回傳Lane或是List<Lane>:

List<Lane> getRootStages()
      Optional<Lane> getRootStage(LaneId laneId)
      Optional<Lane> getLaneById(LaneId laneId)

      理論上使用者不可以透過回傳的Lane或是List<Lane>來修改Workflow的狀態,否則便違反了在DDD中由Aggregate Root確保自身狀態正確性的規定。

      為了回傳不可修改的Lane,首先新增一個ImmutableLane,類別讓它實作Lane介面,如下圖所示。

      ▲代表不可修改的ImmutableLane類別

      眼尖的朋友可能看出來了,ImmutableLane套用Proxy設計模式,它包裝一個原始物件(real subject),並且攔截對原始物件的呼叫。在這裡,所有會改變Lane狀態的方法,例如removeLanes()與addCommittedCard(),ImmutableLane的實作直接丟出一個UnsupportedOperationException,透過這個runtime exception用來物件代表不支援此操作。

      接下來只要修改Workflow(Aggregate Root),讓它回傳內部Entity的時候,也就是回傳Lane或List<Lane>,傳回ImmutableLane即可,如下圖所示。

      ▲修改Workflow讓它回傳ImmutableLane

      ***

      最後寫幾個測試案例驗證Workflow回傳的Lane與List<Lane>真的是不可修改的Lane。

      ***

      後語

      「Aggregate Root回傳不可修改之內部Entity的較佳實作方法到底是什麼?」這件事在Teddy心中放了好一陣子,之前一度使用reflection-util但後來因為前述的原因就沒再使用,因此ezKanban現有的Aggregate Root並沒有嚴格遵守這條要求。

      這幾天Teddy之所以會再注意這件事,是因為前兩天ezKanban團隊與OIS團隊一起mobbing的時候,團隊在event handler裡面寫了一段程式碼,類似:

      workflow.getRootStagebyId(stageId).setWipLimit(WipLimit.UNLIMIT);

      原本設定WIP Limit會產生WipLimitSet領域事件,但是系統跑起來之後卻沒有收到這個領域事件。過了一會兒大家才想起來,啊,不可以直接操作Aggregate內部的Entity來改變Aggregate狀態,要透過Aggregate Root來改變系統狀態。所以程式要改成:

      workflow.setLaneWipLimit(stageId, WipLimit.UNLIMIT);

      ***

      人畢竟是人,如果你問Teddy:「客戶端是不是不能直接修改Aggregate內部的Entity來改變Aggregate狀態?」Teddy會不假思索的回答:「是。」但寫程式的時候,有時一恍神就會寫出下面這種錯誤的程式碼:

      workflow.getRootStagebyId(stageId).setWipLimit(WipLimit.UNLIMIT);

      所以,做人還是不要偷懶,乖乖地讓Aggregate Root回傳不可修改的內部Entity就可以避免這種程式錯誤。

      ***

      友藏內心獨白:犯錯是人的天性,能避免的錯誤還是交由系統來處理。就好像「自主健康管理」有用就不需要集中檢疫了 XD。

      2021年8月20日 星期五

      領域驅動設計學習筆記(19):DDD需要套用Unit of Work (UoW) 設計模式嗎?

      August 19 22:56~August 20 00:29


      背景說明

      昨天Teddy在【搞笑談軟工Facebook社群】提到在DDD中應該是不需要使用Unit of Work (UoW) 設計模式,鄉民A問在DDD不合適使用UoW的原因是什麼?今天談一下這個問題。

      ***

      Unit of Work設計模式

      依據Martin Fowler在《Patterns of Enterprise Application Architecture》的解釋,Unit of Work:

      Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolutions of concurrency problem. (Google 翻譯:維護受業務交易影響的物件列表,並協調更改的寫入和並發問題的解決方案。)

      感覺有一點抽象,Teddy舉一個ezKanban系統的例子來說明Unit of Work。

      ***

      範例說明:ezKanban與看板遊戲

      ezKanban是一個採用DDD與Clean Architecture架構所開發的線上同時多人使用看板系統,圖1展示ezKanban的五個使用案例:Create Board、Create Workflow、Create Stage、Create Swimlane、Crate Card。其中Create Board操作Board Aggregate,Create Card操作Card Aggregate,另外三個使用案例則是操作Workflow Aggregate。


      由於在DDD中Aggregate就是一個交易邊界(transaction boundary),所以每一個使用案例都在各自的交易中處理。從這個角度來看,Aggregate本身就已經是Unit of Work,如果還需要額外套用Unit of Work,就代表有某種需求,需要在Aggregate之外再加上一層,把多個Aggregate放在一個更大的Unit of Work。

      ***

      最近Teddy與ezKanban團隊使用ezKanban系統開發一個新的看板遊戲功能,新寫了Create Kanban Game這個使用案例,請參考圖2。


      Create Kanban Game的實作方式只是呼叫原本寫好的那五個使用案例,此時有鄉民可能會認為:「是不是應該把Create Board、Create Workflow、Create Sage、Create Swimlane、Create Card放在同一個交易中處理以便確保資料的一致性?」如果真的要這麼做,就有可能套用Unit of Work設計模式。

      Unit of Work是把原本不同的工作放在同一個交易(transaction)來處理,這在傳統的CRUD系統(集中式系統)是很常見的需求。但是,在DDD中,不同的Aggregate是透過領域事件(domain event)達到狀態的「最終一致性」(eventual consistency),而不是將這些不同的Aggregate放在另一個Unit of Work中達到ACID。

      在DDD中,如果你願意,Aggregate是可以變成一個最小的部屬單位,也就是說Board、Workflow、Card可能被部署成微服務,因此自然也不適合將它們放在同一個Unit of Work(同一個交易)來處理。

      ***

      如果是同一個Aggregate…

      後來鄉民B進一步問:「如果是在同一個Aggregate的話,是不是就可以套Unit of Work?」

      例如,假設在ezKanban系統中有一個使用情境是要一次新增一百張卡片,實作方式如果是連續呼叫Create Card 使用案例一百次,這樣都是操作同一個Card Aggregate,是不是就可以把這些對於Create Card使用案例的一百次呼叫放在Unit of Work中?

      如果是這種需求,乍看之下覺得要用Unit of Work是很合理的。但是,再仔細思考一下,在DDD中「Aggregate形成交易邊界」這個特性,雖然都是相同型別的Aggregate(都是Card),但這一百張卡片是一百個不同的aggregate instance,所以嚴格講起來每一個aggregate instance應該都只管自己的交易。

      但是,這樣一來「資料正確性」怎麼辦?Teddy覺得在DDD這頂「大帽子」之下,應該儘量朝向分散式處理、最終一致性、交易補償(錯誤處理)的角度來思考系統的行為與狀態,長期而言會比較能夠得讓系統從分析、設計到實作,保持一致性與完整性

      一旦在DDD裡面採用「Unit of Work將多個Aggregate打包處理」,Teddy大膽假設團隊中就會有人的想要在DDD中做到「分散式交易處理」或是「二段式提交(Two-Phase Commit)」的效果,如此一來思考模式又回到傳統的ACID與集中式系統的模式。

      那麼,回到根本的問題,為什要要DDD勒?

      ***

      友藏內心獨白:沒有一定不行,真的想用就用,用了也不用有警察來開罰單。

      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。

      ***

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