l

2022年10月11日 星期二

重辦護照

Oct. 11 11:25~12:02

截圖 2022-10-11 上午11.52.02

▲現場辦理護照的排隊人龍

 

疫情前Teddy和Kay每年至少會固定出國兩次,因為疫情緣故已經兩年多沒出國。最近政府宣布國境解封,護照在明年中即將過期,差不多要重辦護照。

上次辦護照已經是近10年前,記得是某天晚上等Kay下班後一起去外交部領事事務局辦理。幾天前上網查詢,發現申請新護照方便很多,只要先上網填寫資料,然後預約辦理的時間,到時候到外交部領事事務局一樓用機器報到,領取號碼牌之後到三樓窗口辦理即可。

詳細流程可參考:外交部領事事務局個人申辦護照網路填表及預約系統,或申辦護照流程短片

***

護照申請流程網路上資料很多,Teddy就不在重複。這篇簡短分享今天早上去外交部領事事務局辦理辦理護照的幾個心得:

  • 報到時間:Teddy預約的時段是10:30~11:00,抵達現場的時間是10:25。經工作人員告知,可以直接在一樓的公共事務機報到領取號碼牌,不用等到10:30。

 

截圖 2022-10-11 上午11.40.22

▲公共事務機長這樣,圖片節錄自申辦護照流程短片

 

  • 可現場列印護照申請表格:在報到的同時可透過機器選擇列印護照申請表格。Teddy已經事先上網填寫資料並且在家裡用彩色印表機列印出申請表格,所以不需要重複列印表格。如果家中沒有印表的朋友,可以現場列列申請表(表格會帶出網路預約時所填好的基本資料,包含彩色照片,只要將表格印出來簽名即可)。但是這裡有一點要注意,如果你是幫別人(例如親屬)代辦護照,就無法現場列印表格,要在家裡印好,請委託者簽好名帶過來。
  • 過號:拿到號碼牌之後Teddy就立刻搭電梯到三樓,沒想到居然已經過號了,有點傻眼(有兩個網路預約的專門辦理窗口)。請問工作人員,對方很親切地說:「沒關係,你就排在後面,等前面一位辦好後跟工作人員說你過號就可以。」在辦理的過程中,發現也有其他人遇到過號的問題,所以這可能是….正常現象吧。
  • 辦理時間:從抵達現場到辦好護照手續、請郵局寄送護照,一共花了15分鐘,算是非常快了。但是,有很多民眾是現場辦理,沒有網路預約,排隊的人潮就很多。

***

現場排隊的人真的很多,Teddy預估至少要等一小時吧。在離開時Teddy就聽到有已經抵達現場的民眾說:「我們回家網路預約,之後再來好了。」如果不想等太久,網路預約方便很多。

***

友藏內心獨白:難得覺得政府系統做得還不錯。

2022年8月25日 星期四

順序很重要

August 25 17:43~18:21

截圖 2022-08-25 下午6.19.14

 

 

前言

這兩年多和ezKanban團隊一起mobbing,後端套用了Domain-Driven Design(DDD)、Clean Architecture、Event Sourcing、CQRS、TDD、Design By Contract(DBC)、Living Documentation,幾乎全部的後端架構一開始都是Teddy設計的。這原本就是正常現象,因為ezKanban是Teddy帶著學生一起開發的軟體研究專案。

有時候Teddy需要上課無法參加mobbing,課程結束回來之後發現Teddy不在的時候團隊快速完成了一些功能,但是設計方法不是很好,於是Teddy會跟團隊討論一次設計,再看看要如何重購。

***

順序

Scrum有三種角色,Product Owner(PO)、Developer、Scrum Master(SM),用一句話來形容他們的責任分別是:

  • PO:Do the right thing(做對的事情)
  • Developer:Do the thing right(把品質做好)
  • SM:Do it faster(持續改善)

這三件事情,發生的順序是有意義的。首先,要先確保Do the right thing,要了解問題領域中使用者、客戶遭遇到什麼問題?其次,設計出好的軟體來解決這些問題。最後,持續改善整個開發流程。

許多開發人員非常在意電腦是不是最新的,IDE的熱鍵是否用得滾瓜爛熟。這兩件事情,都重要,也都可以縮短開發時間。但是有一件事情比它們還重要,就是你要先確定你做的事情是對的。你的需求、架構、設計,都是對的,否則只是把錯誤的事情做得更快。

***

 

迭代

上述這三件事,它們發生與實踐的過程,並不是線性的,而是迭代的過程。也就是說,先了解需求一點點,然後做點設計、寫點程式,然後改善;然後重複這個過程。Teddy和團隊mobbing的過程,首先強調的一定都是需求,為什麼要做這個功能?先確認需求,然後才討論領域模型與架構要如何滿足這個需求。至於開發流程的改善,工具類只占一部分,它是很重要的一部分但不是最重要的部分。Teddy只會要求IDE基本的功能與快捷鍵要熟悉,開發環境、測試環境、部屬環境儘量自動化與虛擬化。Mobbing的時間,Teddy在意的流程改善,是能不能提升團隊的設計力、讓大家動腦、可以清楚表達一件事。至於IDE熟悉度的提升如果要達到完全不需要使用滑鼠的程度,有興趣的人私底下再自行練習即可。

雖說這三件事是以迭代的情況發生,但還是要注意順序,不要一開始只注意do it faster,也不管打進去IDE裡面的東西到底是什麼,那就本末倒置了。

***

 

友藏內心獨白:垃圾進,垃圾出。

2022年8月17日 星期三

我可能不會用你,Event Sourcing + CQRS?!(下)

August 17 21:54~22:49

截圖 2022-08-17 下午10.07.19

▲圖1:Repository介面

 

前言

在〈我可能不會用你,Event Sourcing + CQRS?!(上)〉Teddy談到Event Sourcing在寫入模型(Write Model/Command Side)並不會比State Sourcing要複雜,甚至更簡單。

但事情通常都是有一好沒兩好,寫入比較簡單,讀取就比較困難,今天討論Event Sourcing在讀取模型的情況。

***

跨Aggregate的資料查詢

在套用DDD(領域驅動設計)之後,Aggregate Root負責發出領域事件,然後透過Repository儲存它的狀態。當採用Event Sourcing時,一個aggregate instance在event store中會有一個event stream用來保存它的所有領域事件。每一個Repository(DDD裡面的Repository設計模式)負責儲存與載入「單一Aggregate」,其介面如圖1所示。

圖2為ezKanban系統core domain的領域模型,它包含Board、Workflow、Card、Tag四個Aggregate。現在問題來了:如何查詢一個Board裡面有多少個workflow?多少張卡片?多少種Tag?」

如果把DDD Repository設計模式定位為「專門在Write Model中用來存取單一Aggregate的介面」,而且Write Model不需要維持「為了查詢而存在的關聯」,那麼在Write Model中,Board並不知道它身上有多少個Workflow以及Tag,而Workflow也不知道它身上每一個Lane有多少張Card。

 

截圖 2022-08-17 下午10.10.36

▲圖2:ezKanban領域模型

 

不用維持雙向關聯之後的Event Sourcing更進一步簡化寫入模型,但查詢就比較困難,特別是針對跨Aggregate之間的查詢。如果不管效率問題,開發人員還是可以透過EventStoreDB內建的Projection功能,讀出領域事件然後在記憶體中「拼出」read model,但這樣執行速度顯然會比較慢。所以在套用Event Sourcing的情況下,針對特別的查詢畫面或報表,通常會特別設計一個View Model以加速讀取速度。

在State Sourcing的情況下,如果是使用關連式資料庫,查詢可以直接下SQL找出所需要的資料。但這並不表示State Sourcing就不會有查詢效率的太慢的問題。同樣地,針對個別查詢畫面,State Sourcing也經常會建立Read Model來加速查詢速度。換句話說,額外建立Read Model並不是Event Sourcing的專利,在State Sourcing系統中,下SQL、Join、Create View都是一種產生Read Model的方法。

***

Event Sourcing到底

傳統上在Event Sourcing系統中套用CQRS,Read Model資料庫的選擇可以和Write Model相同或不同(好像是廢話)。例如Write Model如果用EventStoreDB,Read Model可能用關連式資料庫或NoSQL。但是有另一派的做法是:「既然要Event Sourcing,就Event Sourcing到底,Write Model與Read Model都Event Sourcing。」這是什麼意思?請參考〈事件溯源(11):撰寫JavaScript在EventStoreDB中產生自訂投影〉,Teddy介紹過EventStoreDB可以透過撰寫JavaScript程式讓資料庫自動產生給查詢使用的projection(自訂projection),然後用這些projection來產生Read Model。在這種情況下,Write Database與Read Database就可以合併成一個。

***

 

結論

看到這裡鄉民們可能會覺得:「Teddy你還是沒說Event Sourcing + CQRS是不是比State Sourcing要難啊?」還是那句老話,沒有比較難,只是不一樣。哪裡不一樣:

  • 乍看之下,Event Sourcing寫入比較簡單,讀取比較難,State Sourcing則相反。
  • 實際上則是「能量不滅」,各有各的困難之處。Event Sourcing不須要設計資料庫schema,感覺好棒棒。但設計資料庫schema的問題並沒有消失,只是轉成設計領域事件schema。

Teddy寫這兩篇文章的目的不是要倡導大家使用Event Sourcing,只是要提醒,它就是一種儲存狀態的方式,它沒有比State Sourcing難。如果你找到適合Event Sourcing的應用場景,使用它可以簡化系統設計。如果你的應用場景不需要記錄所有狀態異動,而且你也不熟悉Event Sourcing,那麼就用已經習慣的State Sourcing就好,不用趕流行。

但是,多了解一種狀態儲存方式也沒什麼壞處,難保哪一天合適的應用場景出現了,此時不知道Event Sourcing可能會設計出過於複雜的系統。

***

友藏內心獨白:學習新技術就是為了未來有能力可以看到forces。

2022年8月16日 星期二

我可能不會用你,Event Sourcing + CQRS?!(上)

August 16 10:03~11:33

截圖 2022-08-16 上午10.25.52

▲圖1:Clean Architecture四層架構

 

前言

上周末上完【事件溯源與命令查詢責任分離架構實作班】首發團,有一位認識近十年的老學員告訴Teddy:「這是我上過泰迪軟體所有的課裡面最難的一門課。以前的課我回去都可以直接挑選某部分在工作上應用,這個Event Sourcing加上CQRS我一下子想不到可以用在哪裡。」

Event Sourcing真的比State Sourcing(OR-Mapping)要困難嗎?這次分兩集談一下這個問題,這一集先從寫入(Command/ Write Model)來比較兩者的異同,下一集再來討論讀取。

 

***

只是不一樣

Teddy覺得這位老學員之所以會覺得難,主要的原因是在課程中Teddy把近幾年所學關於Event Sourcing, CQRS, DDD, Clean Architecture的全部重點濃縮在兩天內講完,所以資訊密度比較高。至於Event Sourcing本身Teddy認為並沒有比State Sourcing要難,困難的地方在於你會想應用Event Sourcing的場景通常是在分散式系統或套用微服務架構,是這個「分散式環境」讓你誤以為Event Sourcing比較難,而非它本身真的比較難

和State Sourcing相比,Event Sourcing只是做法不同,並沒有比較難;它們只是一種儲存系統狀態的方法。

 

***

從Clean Architecture看Event Sourcing

請參考圖1的Clean Architecture架構圖:

Entity Layer:Entity Layer首先反映問題領域的重要概念與關聯,理論上這一層的物件根本不管物件如何儲存。但在程式實作面,Event Sourcing對Entity Layer的實作方式的確會產生影響,但也僅止於影響Aggregate Root,程式撰寫風格要改成如圖2的event sourced coding style:任何改變狀態的public method,都呼叫apply方法,並傳給它一個(或多個)代表狀態改變的領域事件。然後在event handler(圖2中的when方法)中實作程式邏輯。這種程式撰寫風格,也可以適用state sourcing,所以也不是說寫成這樣就一定要用event sourcing。

 

截圖 2022-08-16 上午10.37.11

▲圖2:Event Sourced Aggregate Root撰寫風格

***

 

Use Case Layer:請參考圖3,Use Case透過注入的Repository來存取Aggregate,Use Case並不知道所注入的Repository是一個State Sourcing Repository還是Event Sourcing Repository。也就是說,是否採用Event Sourcing並不會影響到Use Case。

截圖 2022-08-16 上午10.58.17

▲圖3:Use Case範例

***

Interface Adapter Layer:請參考圖4,Interface Adapter Layer左方是State Sourced Repository的實作,右方是Event Sourcing Repository實作。從寫入(Write Model)的角度來看,如果是採用OR-Mapping的State Sourced Repository,由於還要寫一堆ORM設定或是下SQL寫入資料庫,實作方式其實比Event Sourced Repository還要複雜,並沒有Event Sourcing的儲存方式比State Sourcing要困難的問題。

 

截圖 2022-08-16 上午11.15.06

▲圖4:Interface Adapter Layer與DB & Driver Layer

 

***

DB & Driver:請參考圖4,DB & Driver Layer左方是State Sourced Database,右方是Event Sourced Database。如果左方採用關聯式資料庫,將Aggregate寫入資料庫表格的同時,需要在同一個交易中把領域事件也一併寫入資料庫的Outbox表格。至於右方的Event Store Database,由於只需要寫入領域事件,不需要像關連式資料庫一樣寫入時套用Transactional Outbox設計模式,所以反而比較簡單。

***

結論

經過以上分析,從讀寫分離的角度來看,Event Sourcing在寫入端其實比State Sourcing還要簡單。但就像Teddy經常說的「能量不滅」,寫入比較簡單,通常讀取就比較困難。下一集從讀取模型來比較Event Sourcing和State Sourcing。

***

 

工商服務

最後打個廣告,10月份【事件溯源與命令查詢責任分離架構實作班】招生中。

***

友藏內心獨白:Event Sourcing是一種第一次會痛,第二次會爽的技術 XDD。

2022年8月5日 星期五

套房還是雅房?

August 05 01:09~02:47

截圖 2022-08-05 上午1.22.57

▲圖1:標準版的ezKanban支援四種報表

 

前言

在外租屋的朋友,如果只租一間房間,一般而言有兩種選擇:套房或是雅房。套房是包含衛浴設備的房間,雅房則沒有專屬的衛浴設備,需要和其他人共用衛浴設備。前者組金比較高,後者比較便宜。

你喜歡套房還是雅房?

如果經濟許可,當然是有專屬衛浴設備的套房比較方便。

但是,從軟體開發的角度來看,軟體開發人員應該更喜歡雅房才對啊!為什麼? 因為這樣大家才可以共用(reuse)衛浴設備。每一個房間都裝一套衛浴設備,是一種duplicated code,這是一種程式開發的怪味道(bad smells),要加以去除才對。

但是,共用就不能獨立自主啦。如果你要洗澡的時候,別人正在洗澡,你就要等待。上一個上完廁所的人衛生習慣不好,沒有沖馬桶,你上廁所的時候就要幫前一個人善後。

所以,要不要共用,不是只有一個「是否會產生duplicated code」這個因素(force),還有其他條件需要考慮。今天Teddy講一個在ezKanban裡面關於共用與去除duplicated code的故事。

***

標準看板報表

如圖1所示,在ezKanban中,支援以下四種報表:

  • Lead Time Distribution Chart(交期分布圖),由GetLeadTimeDistributionChartUseCase負責產生,它透過LeadTimeDistributionChartProjection介面跟資料庫要資料。
  • Control Chart(控制圖),由GetControlChartUseCase負責產生,它透過ControlChartProjection介面跟資料庫要資料。
  • Cumulative Flow Diagram(累積流量圖),由GetCfdUseCase負責產生,它透過CfdProjection介面跟資料庫要資料。。
  • Due Date Performance Chart(到期日績效圖),由GetDueDatePerformanceChartUseCase負責產生,它透過DueDatePerformanceProjection介面跟資料庫要資料。。

因為ezKanban同時支援State Sourcing與Event Sourcing這兩種不同的資料儲存方式,上述因此每一個產生報表的使用案例所使用的Projection介面,各自有兩種實作。State Sourcing的實作使用PostgreSQL資料庫,Event Sourcing的實作使用EventStoreDB資料庫。也就是說,四個報表有八種不同的實作。

***

 

看板遊戲報表

2021年7月,ezKanban團隊基於原本標準看板的領域模型加以擴充,開發看板桌遊,如圖2所示。看板桌遊支援標準版的三種報表(Lead Time Distribution Chart、Control Chart、Cumulative Flow Diagram),另外增加看板桌遊專屬的Financial Report(財務報表)。

 

截圖 2022-08-05 上午1.36.06

▲圖2:ezKanban的桌遊模組所支援的四種報表

 

雖然看板桌遊的前三種報表和標準版的程式邏輯很像,但是有少部分不同。例如對於日期的處理,在標準版中報表的日期是領域事件產生的真實時間,但在看板遊戲中,報表的日期是遊戲中的日期,不是真實世界的時間。例如,圖3是看板遊戲的累積流量圖,X軸的時間分別是Day 8, Day 9到Day 18,這是遊戲設計的時間。圖4則是標準版的累積流量圖,X軸顯示的是真實世界的時間。兩個報表可以透過儲存在資料庫中相同的領域事件資料而產生,但關於時間的處理以及報表呈現的方式則略有不同。

 

截圖 2022-08-04 下午6.15.51

▲圖3:看板遊戲的累積流量圖

 

截圖 2022-08-05 上午1.47.27

▲圖4:ezKanban標準版的累積流量圖

 

ezKanban團隊一開始實作看板遊戲的前三張報表是直接複製標準版的程式碼,加以修改成看板桌遊的報表。因此又增加三個看板桌遊報表的使用案例,以及六支Projection的實作程式。

***

去除重複的程式碼

一開始直接複製原本標準版的程式加以修改,團隊很快就寫好看板桌遊的報表。寫完之後團隊也沒有打算要修改它,因為看板桌遊的需求並不會改變,所以雖然有重複的程式碼,對整個軟體持續開發活動並不會有什麼特別的影響。有一天團隊將開發看板桌遊的經驗整理成論文,寫到處理報表問題的地方,突然覺得:「既然報表邏輯大部分的一樣,是不是可以重構一下,拿掉重複的程式嗎?」

當然可以啊,從寫出Clean Code的角度來看,去除重複性是很基本的要求,於是團隊便安排時間重構這些產生報表的程式。

怎麼重構?首先最大的問題就是看板遊戲的報表顯示的是遊戲中的虛擬時間,因此團隊一開始採用注入一個TimeConverService的作法來轉換時間。如果是標準版的看板,就注入一個NullTimeConverService,如果是看板桌遊的報表,則注入KanbanGameTimeConverService。

但後來ezKanban也重構的「向資料庫要資料」的設計,一開始採用將查詢資料的method寫在一個ReportRepository裡面。後來覺得這樣做不好,因為ReportRepository的介面太肥大,違反了介面分離原則(Interface Segregation Principle)。此外,因為ezKanban也套用了CQRS,在查詢端套用Repository設計模式,容易和命令端套用Repository設計模式產生混淆。因此團隊把查詢端的Repository改為Projection,然後讓一個Projection身上只有一個query method,只負責一種查詢條件(等於套用了GoF的Command設計模式)。

改用Projection之後,為了解決看板桌遊處理時間的問題,團隊又在Projection設計上套了Decorator設計模式,透過KanbanGameDecorator來轉換領域事件的時間。

故事還沒完,除了原本針對State Sourcing與Event Sourcing各有一個Projection的實作,這裡面也有重複的程式碼。從產生報表的角度來看,原本Projection的設計耦合了以下兩種責任:

  • 責任1:從State Sourcing資料庫或是Event Sourcing資料庫找資料的責任。
  • 責任2:找到資料資料(找出的資料是一堆領域事件),利用這些領域事件產生相對應的報表。

不管是State Sourcing或是Event Sourcing,產生報表的邏輯(責任2)都是一樣的,因此團隊又針對責任1設計一個ProjectionPeer介面,讓責任2的程式碼只要在Projection中維持一份即可。至於每一個Projection要到PostgreSQL資料庫或是EventStoreDB撈資料,只要注入不同的ProjectionPeer實作即可。在這裡,等於又套了Adapter設計模式

 

***

值得嗎?

整個重構過程,套了好幾個設計模式,完全去除了重複程式碼,也達到程式碼重用的目的,同時還滿足領域驅動設計、CQRS、Clean Architecture,真的好棒棒啊。

如果真的這麼棒,Teddy就不用寫這一篇文章了。

雖然報表重構的過程還沒結束,但也到了尾聲的階段。回顧整個過程,Teddy學習到一件很重要的事:

針對Write Model的重複程式碼,值得花時間透過重構加以去除。至於Read Model的重複程式碼,除非寫完之後有經常需要修改的可能性,否則重複就重複吧。

***

ezKanban是為了研究與學習而開發的軟體,因此才會設計成「同時支援State Sourcing與Event Sourcing」。因為這個需求,才導致操作資料庫的程式碼出現duplicated code的問題。這個問題,在一般「正常專案」應該是不會出現,因為沒有人吃飽沒事讓自己的商用軟體同時支援不同的儲存方式。

ezKanban為了這個需求,導致整個資料庫存取層的設計變得複雜許多。在一般情況下,在Write Model中,一個Aggregate對應到一個Repository介面;而該介面通常也只需要一個實作,因此不會有duplicated code。在Query Model中,一個查詢對應到一個Projection介面;同樣地,該介面也只需要一個實作,也不會有duplicated code。

所以整個結論就是:

程式碼重用和去除重複性,要看Context來決定。如果鄉民們的專案不需要跟ezKanban一樣同時支援State Sourcing與Event Sourcing(絕大部分應該都屬於這種情況),在資料庫存取層可以用更簡單、更直接的設計就好。

***

友藏內心獨白:套用設計模式的第一個原則就是,不套模式能不能解決問題?

2022年7月26日 星期二

事件溯源(19):在InMemoryRepository實做樂觀鎖

July 26 15:52~16:39

截圖 2022-07-23 下午9.26.00

▲圖1:樂觀鎖測試案例

 

前言

這系列文章原本是Teddy為了製作【事件溯源與命令查詢責任分離架構實作班】課程範例而撰寫,課程範例已經完成,這系列文章也已經連載結束。前幾天Teddy回頭把課程範例改成新的寫法,修改之前先跑測試,居然有一個錯誤!

仔細一看才想起來Teddy之前練習的時候注入在測試案例中InMemoryTagRepository,它沒有支援樂觀鎖,所以原本的樂觀鎖定測試案例會失敗,只要換回正常的Repository就好了,這個問題以前也遇到過。

但是這次Teddy突然想到:「為什麼InMemoryRepository不能支援樂觀鎖?」花了幾分鐘改一下Code,測試案例就通過了。今天就追加一篇,談如何讓InMemoryRepository支援樂觀鎖。

 

 

 

***

實作樂觀鎖

Teddy在<事件溯源(7):樂觀鎖>中介紹過如何在關聯式資料庫與事件溯源資料庫實作樂觀鎖,基本上就是要在Aggregate身上加一個Version欄位,每次儲存Aggregate的時候比對它身上Version的數值與資料庫中的數值是否相等。如果相等,就代表這個Aggregate上次從資料庫讀出之後並沒有其他人寫入,因此它目前的版本是最新的,可以直接儲存到資料庫中。反之,則代表目前Aggregate的版本比較舊,無法儲存,系統要丟出樂觀鎖定失敗例外。

請參考圖1測試案例,從tagRepository根據相同的tagId拿出tagV1與tagV2兩個相同的物件。先把tagV1改名後儲存起來,接著再儲存tagV2,此時tagV2身上的Version數值會小於tagRepository所儲存的數值,因此會丟出RepositorySaveException例外。

 

首先修改InMemoryTageRepository的findById方法,如圖2所示。原本InMemoryTagRepository將Tag儲存在List裡面,findById回傳的記憶體中Tag的參考(reference)。這種直接回傳記憶體參考物件無法測試樂觀鎖,因為圖1中tagV1和tagV2會參考到同一個tag,也就是說改了tagV1會同時改變tagV2的值。所以findById要改成回傳一個新的Tag物件,而不是原本Tag物件的參考。

截圖 2022-07-26 下午4.20.56

▲圖2:修改InMemoryTagRepository的findById方法以支援樂觀鎖

 

 

其次修改InMemoryTagRepository的save方法,如圖3所示。如果要儲存的tag已經存在InMemoryTagRepository,而且它的版本不等於記憶體中的版本,則丟出RepositorySaveException。反之,先把tag從記憶體中移除(如果不移除,相同的Tag會出現在InMemoryTagRepository兩次),然後將它的版本加1,然後把它儲存起來,最後清掉tag身上的領域事件。

截圖 2022-07-23 下午9.29.37

▲圖3:修改InMemoryTagRepository的sava方法以支援樂觀鎖

 

就這樣,這麼簡單。

 

***

結論

本集介紹如何讓InMemoryRepository也具備樂觀鎖,但在這裡Teddy實作的InMemoryTagRepository只支援State Sourcing的儲存方式,並沒有支援Event Sourcing。如果是要實作InMemoryEventSourcingRepository,基本上也不會太困難,應該只需要:

  • 把資料結構由List改成Map<String, List<DomainEvent>>,Map的Key是Event Stream Name,Value是Aggregate的領域是件。至於Aggregate的版本就是List<DomainEvent>的大小。
  • 在儲存Aggregate的時候,不需要更新版本號碼,因為讀取(fndById)的時候Aggregate的版本號碼就是它所屬的List<DomainEvent>的大小。

InMemoryEventSourcingRepository的實作就交給鄉民自行練習。

 

***

友藏內心獨白:這一集算番外篇。

2022年7月19日 星期二

《Clean Architecture實作篇:在整潔的架構上弄髒你的手》程式碼分析

July 19 09:07~10:58

截圖 2022-07-19 上午9.24.56

▲圖1:《Get Your Hands Dirty on Clean Architecture》程式範例目錄結構

 

前言

2021年9月Teddy幫《Get Your Hands Dirty on Clean Architecture》這本書寫了書評(請參考<【還少一本書】Get Your Hands Dirty on Clean Architecture>),前陣子博碩出版社告知這本書中文版將於今年八月上市,請Teddy幫它寫推薦序。

一年多前讀過的英文版,書中有些細節已經不太記得,這幾天花了點時間把中文版用力讀過一次。這本書中文版翻譯得很好,對Clean Architecture有興趣的朋友可以參考。昨天Teddy在部落格文章<再談Clean Architecture實作>提到《Clean Architecture》書中圖22的問題,今天要談《Get Your Hands Dirty on Clean Architecture》這本書程式範例中「不那麼乾淨」的問題。

 

***

程式範例結構

Get Your Hands Dirty on Clean Architecture》的程式範例在此:https://github.com/thombergs/buckpal,鄉民們可以自行下載閱讀。

首先分析程式結構,請參考圖1。可以看出來程式碼依照書中的建議,先package by feature再package by layer。account這個package就代表一個feature(功能),因為這本書的範例程式規模很小,只示範在不同帳戶之間的轉帳功能,所以從目錄結構上看起來會覺得package by feature的味道很薄,但它真的有package by feature。

至於account裡面的domain, application, adapter相當於Clean Architecture的entity layer, use case layer, interface adapter layer。放在package最外層的BuckPalAccplication就是Clean Architecture所說的Main Component。

***

Entity Layer

接著看到Entity Layer,也就是這本書的domain layer程式碼,請參考圖2。在這一層有四個物件:Account, Activity, ActivityWindow, Mondy,書中並沒有套用Domain-Driven Design(DDD;領域驅動設計),也就是說在Entity Layer沒有Aggregate。但是從DDD的角度來看,Account似乎可以當成AggregateRoot。

但這不是重點,重點是作者在Entity Layer用了lombok這個框架用來自動產生getter/setter/constructor等。嚴格講起來在最核心的Entity Layer是不應該相依於外部工具與框架,但lombok在Java社群中是非常流行的工具,使用它可以少寫很多煩人的程式碼。lombok以annotation的形式存在程式碼中,相對而言是比較輕微的入侵。

對於框架的使用,在Clean Architecture書中有提到,使用框架之後你的系統就跟這個框架結婚。新婚的時候可能很快樂,但如果不幸日後鬧翻要離婚,那離婚手續可就很麻煩,你的財產甚至要分對方一半。以ezKanban為例,Teddy並沒有使用lombok,但為了自動序列化/反序列化將物件與JSON互轉格式,在少數Jackson無法自動判別的類別身上還是貼了Jackson annotation,如圖3所示(Jackson是一個處理JSON的工具)。

所以,如果可能在Entity Layer儘量不要使用外部框架或工具,如果真的要用,也要有廝守一輩子的心理準備。

 

截圖 2022-07-19 上午9.35.01

▲圖2:Account程式範例

 

截圖 2022-07-19 上午9.48.01

▲圖3:ezKanban在Entity Layer的類別上使用jackson的annotation

***

 

Use Case Layer

Use Case Layer在書中稱為application layer,請參考圖4。《Get Your Hands Dirty on Clean Architecture》書中套用六角形架構,因此在application layer底下有兩個子package:

  • port:存放application layer對外層依賴反轉的介面,又分為in port和out port。in/out的區分是從application layer的角度來看,如果這個介面是讓外層(例如web controller)用來呼叫內層,它就是一個in port(由外往內);如果它是讓application layer的物件呼叫外部服務的介面,例如用來存取資料庫的repository,這就是一個out port(由內往外)。
  • service:實作port的物件稱為service,例如實作SendMoneyUseCase介面的物件叫做SendMoneyService,你也可以把它叫做SendMonyUseCaseImpl,看你喜歡何種命名方式。

 

截圖 2022-07-19 上午9.57.19

▲圖4:Use Case Layer (Application Layer)結構

 

接下來Teddy要開始挑毛病了,請參考圖5,SendMoneyUseCase是書中主要用來當作範例解釋的使用案例,它只有一個sendMoney方法,輸入參數是SendMoneyCommand(請參考圖6),輸出為boolean。

 

截圖 2022-07-19 上午10.06.50

▲圖5:SendMoneyUseCase程式碼

 

截圖 2022-07-19 上午10.11.47

▲圖6:SendMoneyCommand程式碼

 

把圖5與圖6對照到Teddy昨天<再談Clean Architecture實作>畫過的圖7:

  • Input Port:SendMoneyUseCas
  • Input Data:SendMoneyCommand
  • Output Data:boolean

 

看到這裡鄉民有沒有發現什麼問題?

截圖 2022-07-19 上午10.09.11

▲圖7:Teddy修正《Clean Architecture》書中圖22之後的結果

 

請參考圖8,在《Clean Architecture》書中提到跨層的資料結構通常是簡單的資料結構,SendMoneyCommand是跨越application layer(use case layer)與adapter layer的物件,但是它身上的屬性卻有AccoundId, Money這兩個位於Entity Layer的Value Object。也就是說Entity Layer的物件透過SendMoneyCommand傳遞到第三層,這麼做雖然沒有違反《Clean Architecture》的依賴原則(相依性由外往內),但是卻違反了跨層原則,這個由SendMoneyUseCase所形成的Input Port(Input Boundary),不是一個完整的雙向介面。

在《Clean Architecture》書中提到理想上介面應該是雙向隔離,一開始要採用單向隔離的介面也可以,日後再隨需要調整成雙向介面。在目前的ezKanban中,Entity Layer的物件傳遞離開Use Case Layer之前一定都經過轉換,往UI層轉成DTO物件,往資料庫層轉成Data物件,領域事件傳遞到其他Bounded Context則是轉成RemoteDomainEvent物件,

 

截圖 2022-07-19 上午10.25.44

▲圖8:《Clean Architecture》中文版第172頁

***

Interface Adapter Layer

最後看到位於Interface Adapter Layer的SendMoneyController程式碼,如圖9所示。可以很清楚看出來,SendMoneyController在第26行產生一個SendMoneyCommand物件,然後位於Entity Layer的AccountId與Money物件也被第三層(Interface Adapter Layer)的SendMoneyController給參考到。如同Teddy在上一小節所提到的,SendMoneyCommand是Input Port介面上的資料結構,它應該使用基本資料型別就好,不要使用Entity Layer的物件,以免造成系統架構不乾淨

 

截圖 2022-07-19 上午10.38.21

▲圖9:SendMoneyController程式碼

***

結論

Get Your Hands Dirty on Clean Architecture》是一本好書,但如同所有好書一樣,讀書時必須抱持著「盡信書不如無書」的態度,如此才可深入閱讀並增進自己的思考能力。

書中範例其實還有其他問題Teddy也沒時間逐一指出,例如範例程式包含一個GetAccountBalanceService程式,請參考圖10。Teddy以為可以看到CQRS裡面的Query範例,但是這個程式有介面(GetAccountBalanceQuery)也有實作(GetAccountBalanceService),但卻沒有使用它的Controller(範例程式中沒有任何人使用到GetAccountBalanceService)。請注意它的介面回傳Money,一個位於Entity Layer的物件。它有被轉成DTO往UI傳遞嗎?誰來做這個轉換?《Clean Architecture》書中的Presenter怎麼實作?這些問題都沒有包含在程式範例裡面,鄉民們看完之後可能還是不知道怎麼做。

 

截圖 2022-07-19 上午10.47.30

▲圖10:GetAccountBalanceService程式碼

 

最後打個廣告,如果想知道完整又乾淨的《Clean Architecture》架構與實作方法,歡迎參加【領域驅動設計與簡潔架構入門實作班】。

***

友藏內心獨白:程式碼很少的時候都看不出問題。