l

2020年8月21日 星期五

三種團隊互動模式

August 21 18:15~19:01


前言

Teddy跟著ezKanban團隊兩年的時間,帶著幾位北科資工研究生開發ezKanban系統。當初是以DDD與Clean Architecture為研究主題當作切入點,這兩年來雖然有點進度,但軟體本身卻還沒達到Teddy認為可釋出的標準。

這個兩年的lead time真的有點長,今年七月初放暑假時,Teddy決定利用暑假時間和ezKanban團隊採用remote mobbing的方式一起開發。學生在學校實驗室,Teddy在家裡,透過Skype分享桌面的方式開發ezKanban,一個多月下來頗有進展。

***

幾個禮拜前在Amazon亂買書,買了一本《Team Topologies: Organizing Business and Technology Teams for Fast Flow》,有一天晚上睡不著翻了一下,沒想到還挺有趣的。書中提到四種團隊型態以及三種互動模式,特別適合軟體開發公司。今天用Teddy這兩年來和ezKanban團隊的合作模式,解釋書中提到的三種互動模式。

***

X-as-a-Service

將另一個團隊視為一種服務來使用,團隊之間只有最少的協作。這種情況發生在學生遇到問題詢問Teddy的時候,此時Teddy-as-a-Service,提供學生某種解答,這個過程中雙方並沒有很多協作關係。

***

Facilitating

協助另一個團隊釐清阻礙,這種情況發生在兩周一次的sprint review。Teddy會看學生的設計與程式碼,觀察有沒有什麼設計問題是學生沒有看出來的,並與他們討論並訂定後續修正計畫。下個sprint review會繼續相同步驟,經過迭代的過層,系統設計品質越來越好。

但是,因為兩周review一次兩小時,說真的能夠看到的程式碼真的不多,因此每個迭代的增量幅度相對來講就比較有限。另外一個問題是,因為學生還在學習的過程,很多Teddy沒有看到的地方,雖然程式可以動,但設計通常都存在一些大大小小的問題,長期而言阻礙系統的可維護性。

***

Collaboration

兩個團隊彼此緊密合作。在今年暑假之前,Teddy和ezKanban團隊的關係幾乎不存在Collaboration(協作)。之前也幾度思考過是不是跟學生一起寫程式,但又擔心如此一來學生偷懶,過於依賴Teddy,搞到後來變成Teddy幫他們做碩士論文研究 Orz,因此就作罷。

但隨著ezKanban慢慢成形,但又無法達到Teddy心中的釋出要求,有種恨鐵不成鋼的遺憾。幾經思考,加上今年因為因為疫情的緣故,泰迪軟體生意比較清淡,Teddy空閒時間也比較多,因此決定「撩落去」,利用暑假時間一起合作開發。

***

成本不同

這三種互動模式,適合解決的問題不同,成本也不同。對Teddy而言,開發軟體如果可以和團隊緊密合作當然是效果最好的。畢竟Teddy也比學生多活了20幾年,還是比他們更能夠看出設計不合適之處。

除了學生獲益,Teddy也從這個過程中獲得很多design bad smells的寶貴範例,平常可能想破頭都不一定生的出來這些例子。這是一個雙方都互相學習的過程。

很多時候,想要成事,還是需要走到前線,親自動手。

***

友藏內心獨白:If you stop coding, you stop learning, by Kent Beck.

2020年8月20日 星期四

Clean Architecture(7):套用CQRS簡化Presenter與View Model

August 20 20:50~22:45


背景說明

《Clean Architecture》書中有一張圖解釋Clean Architecture在網頁程式的實作,如圖1所示。

▲圖1:典型的Clean Architecture實作

Use Case Interactor定義了Input Boundary、Output Boundary以及Input Data和Output Data,這些類別與介面都位於Use Case階層。

其中Output Boundary介面由Presenter實作。Presenter位於Interface Adapters,負責產生View所需要的資料,稱為View Model。

以上分層與類別/介面的關係看起來很簡單,在撰寫程式碼時,一個Use Case對應到一組Input與Output,Input由Use Case實作,Output由Presenter實作。理論上,一個Use Case會對應到一個Presenter。

***

太多Presenter

如果完全依照圖1的方式去開發,有多少個Use Case就會有多少個Presenter與View Model。圖2是ezKanban系統關於操作Board(看板)所設計出來的多個Presenter與View Model其中,其中包含:

  • addermember:加入組員
  • changetitle:修改標題
  • create:新增看板
  • delete:刪除看板
  • enter:進入看板
  • get:取得看板id與名稱
  • getcontent:取得整個看板的資料,包含裡面的Workflow
  • leave:離開看板


▲圖2:ezKanban系統為了操作Board所設計的多個Presenter與View Model


圖3為CreateBoardViewModel、RenameBoardViewModel以及DeleteBoardViewModel的內容,長得一模一樣,都只包含boardId這個資料。


▲圖3:三個View Model內容都很像


圖4的BoardContentViewModel長得和其他人不一樣,有比較多資料。


▲圖4:BoardContentViewModel

***

區分Command和Query

Command是會修改系統狀態但不回傳資料的操作,Query是不會改變系統狀態但會回傳資料的操作。Command query separation(CQS)是由Bertrand Meyer所提出的設計方法,最早應用於他自己發明的Eiffel語言中。

Command query responsibility segregation (CQRS) 受到CQS的影響,將這個概念套用在軟體架構上。

請注意看圖2裡面關於Board的操作,可以套用CQRS的精神,將使用案例分成兩大類:

  • Command:addmember、changetitle、create、delete、enter、leave
  • Query:get、getcontent

套用CQRS之後理想狀況是所有的Command共用相同的Output、Presenter以及View Model。雖然理想上Command不應該回傳任何值,但實務上在分散式系統中,可以讓Command回傳有限的值,例如所產生物件的id,以及執行的結果(成功或失敗)與錯誤訊息。


▲圖5:可以給Command共用的Presenter與View Model


▲圖6:簡化後Board功能只需要兩個給Query使用的Presenter與View Model

***

友藏內心獨白:Clean Architecture + CQRS = 好棒棒。

2020年8月19日 星期三

再談Clean Architecture三原則

August 19 08:45~10:00;12:56~13:38


2018年Teddy寫了幾篇介紹Clean Architecture的文章如下,其中有三篇提到Clean Architecture三原則:分層相依性跨層,今天再一次一起討論這三個原則。

***

分層原則

如圖1所示,Clean Architecture有四層:Entities、Use Cases、Interface Adapters、Frameworks & Drivers。分層原則乍看之下最簡單也最沒爭議,但實際上有些物件要放在哪一層還是存在灰色地帶。

最核心的Entities存放domain model物件。如果對照領域驅動設計(Domain-Driven Design;DDD),Aggregate、Entity、Value Object、Domain Service就放在這一層。應用程式邏輯放在Use Cases這一層,也就是DDD裡面的Application Service。

先看這兩層就好,請問DDD的Repository要放在哪裡?大部分的人覺得要放在Use Cases這一層,但也有人主張要放在Entities。所以,雖然Clean Architecture這四層大方向來講區分得很清楚,但在實作的時候有一些「支援物件」,像是剛剛提到的Repository以及Mapper等實際上要放在哪裡,還是有一些討論的空間。


CleanArchitecture

▲圖1:《Clean Architecture》建議的四層架構,畫成同心圓(圖片來源在此)。

***

相依性原則

原始碼的依賴方向必須由外往內、由低層往高層。也就是說Entities層的物件只可以參考(依賴)同一層的其他物件,不可以參考其他層的物件。Use Cases層只可以往內參考Entities層以及自己這一層的物件,不可以往外參考其他層的物件,依此類推。

但是,如果Use Cases需要將Aggregates儲存到資料庫,那一定要參考到最外層的資料庫物件啊,這怎麼辦?如圖2所示,Clean Architecture透過依賴反轉(Dependency Inversion Principle;DIP)確保相依性原則可以被遵守。


▲圖2:透過依賴反轉做到相依性由外往內。

現在問題來了,圖2的Workflow aggregate透過AddWorkflowUseCase把Workflow儲存到MySQL資料庫,這樣子有沒有違反相依性原則?

雖然SqlWorkflowRepository與MySQL都跨層直接參考Workflow,但相依性的方向還是由外向內,因此並沒有違反相依性原則。但是,這麼做違反了Clean Architecture的第三個原則:跨層原則。

相依性原則要完全遵守也是不容易的一件事,例如Entities層的Aggregate要發Domain Event(領域事件)需要用到event bus公具程式,最簡單的方式是找人家已經寫好的工具來使用,但是這樣就違反了相依性原則。為了滿足相依性原則,就要幫著個event bus工具程式定義介面,透過依賴反轉做到相依性原則。有時候定義依賴反轉介面很簡單,但有時候卻不容易。

***

跨層原則

當Entities層的物件跨越離開Use Case層往外傳遞時(傳到UI或是DB或其他外部系統),不要直接把Entities層的物件傳出Use Case層,而是要在Use Cases層定義往外傳遞的介面與資料結構。簡單說就是把Entities層的物件轉成DTO(Data Transfer Object)再往外傳遞。

如果直接把Entities層的物件往外傳,例如直接傳給UI,很有可能因為UI端的需求,導致回頭影響Entities層的物件。也就是說,presentation logic影響了domain logic。為了做到分層負責與單一責任,所以物件跨層的時候要轉一次資料型態。

***

潔癖

完全做到Clean Architecture的三個原則,系統架構就會變得很乾淨,但實作上卻會很麻煩。就好像跟一個有潔癖的人住在一起,居住環境一定很乾淨,但你可能會被對方煩死。一天要擦三次地板、衣服髒了要馬上洗、裝冷飲的杯子要用杯墊,不可以讓水滴到桌上、回家要馬上消毒等等。

這幾年實作Clean Architecture的經驗,Teddy覺得只要做到以下幾點即可,其餘部分可以不用那麼「乾淨」:

  • 相依性原則在大部分的情況下都要遵守,特別是Entities層與Use Case層。如果真的需要使用到一些工具軟體且又不能不想透過依賴反轉滿足相依性原則,這樣的特例不能太多,且要自己心裡有數,承受這樣的依賴關係。
  • Interface Adapters層經常與框架緊密相關,例如Web Controller和所使用的Web Container框架緊密相關。在這種情況下,要做的完全的依賴反轉是很痛苦的一件事。所以Interface Adapters層放生,就讓它和框架緊密耦合吧。
  • 把Use Cases分成Command與Query可以簡化很多Interface Adapters層的物件設計。Command是會改變系統狀態但不回傳值(或只回傳物件的id等特定資料)的操作,Query是回傳資料但不會改變系統狀態的操作。基本上Command可以共用同一個Presenter與View Model,只有Query需要客製化的Presenter和View Model。區分Command和Query之後,Interface Adapters層可以清爽許多。

***

友藏內心獨白:沒有絕對的乾淨,無塵室也是有灰塵。

2020年8月18日 星期二

領域驅動設計學習筆記(10):實作Repository

August 18 10:00~13:35


背景介紹

在《Clean Architecture》書中建議使用Gateway來存取物件,透過依賴反轉原則,避免領域物件(Domain Model,在Clean Architecture中稱為Entity Layer)與使用案例層對資料存取層產生直接依賴。

Gateway是一個比較抽象的名詞,Teddy在實作Clean Architecture的同時也套用了領域驅動設計(Domain-Driven Design;DDD),因此採用領域驅動設計所建議的Repository設計模式來實作Gateway。

在DDD中,每一個Aggregate會對應到一個Repository負責物件持久化的工作。

接下來討論幾種實作Repository的方法,以及各自的優缺點。

***

Take 1,一對一

實作Repository最直接的方式就是先幫每一個Aggregate宣告一個相對應的Repository介面,然後實作這些介面。圖1為Clean Kanban系統的三個Repository(Teddy開發的看板系統)。

▲圖1:一個Aggregate對應一個Repository

Teddy幫每一個Repository提供兩個版本的實作物件:

  • InMemoryRepository:把Aggregarte儲存在記憶體中,方便測試使用,如圖2所示。
  • SerializableRepository:透過Java的serialization功能直接把物件儲存在檔案中,如圖3所示。

▲圖2:ImMemoryBoardRepository


▲圖3:SerializableBoardRepository

第一種方法最直接也很簡單,相信大家都看得懂。

***

Take 2,泛型

看到這裡鄉民們應該會想:「每一個Aggregate都宣告一個,Repository介面,這樣太麻煩了,怎麼不用泛型(generic programming)?」


▲圖4:採用泛型Repository介面


圖4為採用泛型的Repository介面,之後只要需一個InMemoryRepository實作便可讓所有的Aggregate使用,如圖5所示。

▲圖5:採用泛型的InMemoryRepository

***

Take 3,使用JPA

前兩種方式都需要自己撰寫實作Repository的程式碼,如果想偷懶使用Java 持久化 API (Java Persistence API;JPA),讓Hibernate自動產生Repository實作。


▲圖6:使用JPA讓SpringBoot透過Hibernate自動產生Repository實作

這個做法省去了自己撰寫操作資料庫程式的麻煩,但存在三個問題:

  • 有些資料庫操作沒有定義在標準的Repository介面裡面,需要自己撰寫Java持久化查詢語言 (Java Persistence Query Language;JPQL)客製化查詢條件。例如圖6的WorkflowRepository繼承自CrudRepository介面,而CrudRepository只有findById,可以透過workflow id 找到workflow。如果需要透過board id找到workflow,就需要自己增加一個getWorkflowsById的方法,然後用@Query annotation來撰寫自訂查詢條件。這原本也不是什麼大問題,但如果查詢結果無法完全用JPQL表達,而是需要在從資料庫回傳資料後再透過Repository處理,那就沒辦法了(因為WorkflowRepository只是一個interface,它的實作是程式執行時動態產生)。
  • 第二個問題來自於Clean Architecture,Repository儲存的對象是Aggregate,也就是圖6中的Workflow。但直接把位於Entity Layer的Workflow物件丟到最外層的DB & Drivers,違反了Clean Architecture的跨層原則。
  • 第三個問題也是因為套用Clean Architecture才會有的困擾。為了讓JPA自動產生資料庫對應程式碼,必須在Entity(domain model裡面的entity類別)貼上JPA的@Entity annotation,如圖7所示。雖然@Entity是JPA標準的annotation,但JDK並沒有內建這些annotation。為了使用它必須要加入對框架的相依性,例如要引用Hibernate。如此一來,便違反了Clean Architecture的相依性原則:「高層不可以相依於底層(原始碼的相依性只可由外往內,由底層往高層)」。此外,在domain model的entity類別身上貼了一堆「狗皮膏藥(annotation)」,對於有潔癖的人來講,也是無法接受的一件事XD。


▲圖7:貼上@Entity讓JPA知道這是一個需要持久化的物件

***

Take 4,Bridge設計模式

讓JPA直接產生Repository實作雖然方便,但卻少了一些自行撰寫程式的彈性,請參考圖8。

▲圖8:Client直接相依於Repository介面


這個問題的根本原因是我們把JPA所產生的Repository直接丟給客戶端使用,如果可以在客戶端和Repository之間增加一個間接層,就可以透過這個間接層手動增加一些彈性的操作,請參考圖9。這個設計,就是Bridge設計模式。


▲圖9:在Client與JPA產生的Repository實作之間增加一層。原本的JPA Repository改名為RepositoryPeer,客戶端對應的是新增加的Repository介面。

***

最後實作程式碼如圖10所示。

▲圖10:套用Bridge設計模式之後的Repository。


眼尖的鄉民可能注意到圖10中的一些細節:

  • WorkflowRepository所儲存的物件類別是Workflow,但透過WorkflowRepositoryPeer儲存到資料庫裡面的是WorkflowData。這是為了不要將Entity層的Workflow直接傳到第四層的Database & Driver。
  • 為了把Workflow轉成WorkflowData,以及反之把WorkflowData轉成Workflow,還套用了Mapper設計模式。這個資料傳換由WorkflowRepositoryImpl所負責。這也是套用Bridge的好處,在呼叫peer之前以及之後,可以插入一些程式邏輯。
  • 因為最後儲存到資料庫的是WorkflowData類別,因此只要在WorkflowData類別貼JPA annotation即可(參考圖11),位於entity 層的Workflow保持乾淨的原貌。


▲圖10:WorkflowData類別

***

友藏內心獨白:終於寫完了。