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

截圖 2022-11-04 下午1.04.39

▲圖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程式範例,如圖二所示。但這樣寫有沒有比較好,要再花時間想一下。

***

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