Slide 1

Slide 1 text

導入 Domain-Driven Design 的最佳時機 The best time to use Domain-Driven Design in your project

Slide 2

Slide 2 text

蒼時弦也 Software Developer https://blog.aotoki.me

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

困難 敏捷 架構 導入

Slide 5

Slide 5 text

困難 敏捷 架構 導入

Slide 6

Slide 6 text

困難 敏捷 架構 導入

Slide 7

Slide 7 text

困難 敏捷 架構 導入

Slide 8

Slide 8 text

起點 為何開始領域驅動設計

Slide 9

Slide 9 text

Ruby on Rails 社群很少直接討論 Domain-Driven Design 自己在 2020 年之前,完全不知道有這一套理論

Slide 10

Slide 10 text

另一方面 Ruby on Rails 有不少「慣例」讓開發變得簡單的同時, 更難以將 Domain-Driven Design 的觀念放到裡面

Slide 11

Slide 11 text

慘痛經驗 因為設計失誤造成問題

Slide 12

Slide 12 text

2019 年參與一套非常複雜的系統開發,業主說資料表都設計好,只 要解決他們最複雜客戶其他就能支援,結果完全無法支援其他客戶

Slide 13

Slide 13 text

在沒有了解使用者需求下設計系統代價極大,這次慘痛的經驗讓自 己下定決心更關注系統設計的議題

Slide 14

Slide 14 text

挑戰 實踐過程中要克服的事情

Slide 15

Slide 15 text

Knowledge 純自學的前提,從讀完書到能夠熟練應用,大致上花費兩年時間實作、補充知識

Slide 16

Slide 16 text

Team 說服團隊使用需要所有人都具備知識,並且積極的進行討論才有機會成功

Slide 17

Slide 17 text

Legacy Code 想要修改現有系統,在大型專案中很容易互相影響,要先切出邊界

Slide 18

Slide 18 text

Meetings Event Storming 總是進行很久,總是有考慮不完的問題

Slide 19

Slide 19 text

Standard Rails 沒有官方標準和正式寫法,Golang 能找到三種以上的實作樣板

Slide 20

Slide 20 text

想要一次性解決問題非常困難,需要漸進式的把問題解決才有機會

Slide 21

Slide 21 text

敏捷 小增量的進行改進

Slide 22

Slide 22 text

Epic 1 - Q1 Epic 2 - Q2 過去在規劃專案時,時間較長範圍也較多,做系統分析時就會很費時

Slide 23

Slide 23 text

Epic 1 - Q1 Epic 2 - Q2 Feat 1 - W1 Feat 2 - W3 Feat 3 - W1 Feat 4 - W2 從敏捷的角度,我們把一次改版切割成以功能單位來看

Slide 24

Slide 24 text

Epic 1 - Q1 Epic 2 - Q2 Feat 1 - W1 Feat 2 - W3 Feat 3 - W1 Feat 4 - W2 Feat 5 - W3 也可以很彈性對應變化,調整某個功能的優先順序

Slide 25

Slide 25 text

當需要討論範圍變小,耗費時間的設計會議就能夠縮短時間

Slide 26

Slide 26 text

重構 當一切快速進行,必有代價

Slide 27

Slide 27 text

快速迭代必然會需要持續的重構,我們最終實作跟預測差異多少, 是否更有價值?

Slide 28

Slide 28 text

Event Storming 我們使用 Event Storming 對系統開發的幫助是什麼?

Slide 29

Slide 29 text

透過 Event Storming 能夠幫助我們全面的了解系統,避免在開發 過程中失去控制

Slide 30

Slide 30 text

然而,在快速變化、有許多不確定性的情境中,還能在(前期)有 幫助嗎?

Slide 31

Slide 31 text

敏捷開發要能夠成立所需的條件非常多,也包括工程師的自律以及 需要有一定水準的能力等問題

Slide 32

Slide 32 text

規格 透過約定確保底線

Slide 33

Slide 33 text

利用「驗收」規格來確保成果是可預期的,讓 User Interface 跟使 用者、客戶期待的一致

Slide 34

Slide 34 text

UI Application 即使系統實作完全失控,至少使用者還能順利操作跟使用

Slide 35

Slide 35 text

Command Process / System Event 從 Event Storming 的角度看是很初期的實現,然而關鍵的 Command / Event 有被找出來

Slide 36

Slide 36 text

我們是否能夠將 Event Storming 分階段進行,是否可以從 User Story 中推導出 Event Storming 的內容?

Slide 37

Slide 37 text

介面 保持系統的彈性

Slide 38

Slide 38 text

經常跟 Domain-Driven Design 一起談的是 Clean Architecture, 這背後是如何保持彈性的技巧

Slide 39

Slide 39 text

Input Function Output 在生活經驗中,我們大多可以意識到一個動作會有「輸入」跟「輸出」成對

Slide 40

Slide 40 text

Command Process / System Event 對應到 Event Storming 上也可以得到相同的結構

Slide 41

Slide 41 text

// Golang // ... package type interface error error { (context.Context) (context.Context) } daemon Service Start Stop

Slide 42

Slide 42 text

// Golang package type struct func * error return func * error return { http.Server } (s HttpServer) (ctx context.Context) { s. () } (s HttpServer) (ctx context.Context) { s. (ctx) } main HttpServer Start Stop ListenAndServe Shutdown

Slide 43

Slide 43 text

// Golang // ... package import func := & & if := != ( ) () { d daemon. ( HttpServer{ Addr: }, GrpcServer{ Addr: }, ) err d. (context. ()); err { (err) } } main myapp/pkg/daemon main " " ":8080" ":8081" New Run Background nil panic

Slide 44

Slide 44 text

在這個例子,我們約定了 Daemon 會使用 Service 介面來啟動服 務,如此一來我們只要符合條件就能夠被 Daemon 使用

Slide 45

Slide 45 text

Scenario When Then : I click I can see When user click "Toggle" button and show "Hello" "Toggle" "Hello" # 介面:Button() # 輸入:ClickEvent(Label="Toggle") # 輸出:String("Hello")

Slide 46

Slide 46 text

從文件、規格的角度來看,我們實際上都是在定義介面。

Slide 47

Slide 47 text

架構 有了介面就能劃分出邊界

Slide 48

Slide 48 text

介面反應了邊界(Boundary)因此我們有了架構上的邊界,或者在 Domain-Driven Design 中的上下文(Context)邊界

Slide 49

Slide 49 text

Presenter Application Domain Model 從 Layered Architecture 作為例子,每一層之間都有一個約定的介面存在

Slide 50

Slide 50 text

User HTTP Protocol Presenter 以網站來說,使用者跟 Presenter 約定的介面可能會是 HTTP 協定

Slide 51

Slide 51 text

Presenter User Flow Application Presenter 跟 Application 共同約定的介面是使用者流程

Slide 52

Slide 52 text

Application Business Logic Domain Model Application 跟 Domain Model 共同約定的介面是商業邏輯

Slide 53

Slide 53 text

介面有點像是「黏著劑」想要將金屬跟玻璃緊密的黏在一起,就需 要使用正確的黏著劑,軟體系統也是類似的

Slide 54

Slide 54 text

然而,如果黏著劑想要對應多種材質就很可能造成不牢固、難以清 理等問題(跨層的依賴)

Slide 55

Slide 55 text

依賴 用介面來控制依賴

Slide 56

Slide 56 text

想實現快速迭代的目標,就需要容易重構、擴充,那麼將物件的耦 合(依賴)控制在最小就變得很重要

Slide 57

Slide 57 text

Presenter Application Domain Model Infrastructure 以 Layered Architecture 常見的介紹方式,很難說明 Infrastructure 的依賴關係

Slide 58

Slide 58 text

Presenter Application Domain Model Infrastructure 我認為這張圖的樣子更加合理一些,然而 Domain Model 該依賴其他物件嗎?

Slide 59

Slide 59 text

Presenter Application Infrastructure Domain Model 在我的經驗裡面,Domain Model 是沒有依賴的

Slide 60

Slide 60 text

大多數時候,只要遵循「只依賴相鄰的類型」以及「保持單向依 賴」兩個原則,加上善用介面,大多能在必要時很好的進行抽換

Slide 61

Slide 61 text

導入 有策略地進行應用

Slide 62

Slide 62 text

在經驗中,初期就開始引入 Domain-Driven Design 的概念會讓專 案更容易維護,然而有許多限制,因此需要轉換成工作上的原則

Slide 63

Slide 63 text

案例 近期的案例分享

Slide 64

Slide 64 text

這是一個活動的報到 App 後端,當參加者查詢狀態時會顯示活動資 訊,並且紀錄上一次使用的時間

Slide 65

Slide 65 text

Attendee Get Status AttendeeInfo GET /status?token=[TOKEN]

Slide 66

Slide 66 text

Attendee Get Status AttendeeInfo Actor 是 Attendee,透過 `token` 參數識別

Slide 67

Slide 67 text

Attendee Get Status AttendeeInfo 我們要實作一個功能,查詢以及更新資訊

Slide 68

Slide 68 text

Attendee Get Status AttendeeInfo 回傳的結果是 AttendeeInfo 並且包含「名稱」和「票種」

Slide 69

Slide 69 text

#language:zh-TW 功能 場景 假設 | token | name | type | | 1234567890 | 蒼時 | 一般票 | 當 那麼 : : 有一張票券 我發出 GET 請求到 我會看到 JSON 格式的回應 票券資訊 當查詢票券資訊時,可以看到名稱和票種 "/status?token=1234567890" """ { "name": "蒼時", "type": "一般票" } """

Slide 70

Slide 70 text

Presenter Application Infrastructure Domain Model 從 Presenter(使用者介面) 開始處理

Slide 71

Slide 71 text

interface extends : interface : : export async function : : return { ; } { ; ; } ( ) < > { { name: , type: , }; } AttendeeInfoRequest HttpRequest AttendeeInfoResponse statusHandler AttendeeInfoRequest Promise AttendeeInfoResponse token name type request string string string // ... '蒼時' '一般票'

Slide 72

Slide 72 text

Presenter Application Infrastructure Domain Model 接著再處理 Application(流程)的機制

Slide 73

Slide 73 text

interface : : & export async function : : const = const = await return { ; } HttpRequest ( ) < > { { , } request; attendeeUsecase. (token); { name: attendee.name, type: attendee.type, } } AttendeeInfoRequest AttendeeUsecase statusHandler AttendeeInfoRequest Promise AttendeeInfoResponse getAttendee token attendeeUsecase request string token attendeeUsecase attendee // ... // ...

Slide 74

Slide 74 text

interface : : export class async : : return { ; ; } { ( ) < > { { name: , type: , }; } } AttendeeInfo AttendeeUsecase getAttendee Promise AttendeeInfo name type token string string string // ... '蒼時' '一般票'

Slide 75

Slide 75 text

Presenter Application Infrastructure Domain Model 根據分析的結果,實作 Domain Model(領域模型)必要的部分

Slide 76

Slide 76 text

// ... // ... export class public readonly : public readonly : private ?: constructor : : = = = new { ; ; ; ( , ) { .name name; .type type; } () { ._lastUsedAt (); } } Attendee AttendeeType Date AttendeeType touch name type _lastUsedAt name type string string this this this Date

Slide 77

Slide 77 text

Presenter Application Infrastructure Domain Model 進一步把 Domain Model 提供的功能加入到 Use Case 中

Slide 78

Slide 78 text

export class private : async : : const : = await await return { ; ( ) < > { .attendees. (token); attendee. (); .attendees. (attendee); { name: attendee.name, type: attendee.type, }; } } AttendeeUsecase AttendeeRepository getAttendee Promise AttendeeInfo Attendee findByToken touch save // Next // ... attendees token string attendee this this

Slide 79

Slide 79 text

Presenter Application Infrastructure Domain Model 將底層依賴最後再做判斷,也能減少實作受到底層依賴的限制(如:資料表)

Slide 80

Slide 80 text

type = : : export class async : : const = return new { ; ; }; { ( ) < > { .database . ( , [token]) . < >(token); ({ name: res.name, type: res.type, }); } } AttendeeSchema PostgresAttendeeRepository findByToken Promise Attendee prepare first AttendeeSchema Attendee name type token string number string res this // ... '[SQL]'

Slide 81

Slide 81 text

AttendeeInfoRequest / AttendeeInfoResponse Application Infrastructure Domain Model Presenter 的介面是 API Interface 的定義

Slide 82

Slide 82 text

AttendeeInfoRequest / AttendeeInfoResponse getAttendee(token) / AttendeeInfo Infrastructure Domain Model Application 的介面是 UseCase 的方法和回傳

Slide 83

Slide 83 text

AttendeeInfoRequest / AttendeeInfoResponse getAttendee(token) / AttendeeInfo Infrastructure Attendee Domain Model 的介面是 Entity、Service 等等物件

Slide 84

Slide 84 text

AttendeeInfoRequest / AttendeeInfoResponse getAttendee(token) / AttendeeInfo AttendeeRepository Attendee Infrastructure 的介面會照 Presenter / Application 需要而定, 沒辦法契合時則會實作 Adapter 來對應

Slide 85

Slide 85 text

綜合敏捷、Clean Architecture 和 Domain-Driven Design 的優 點,在一些地方作出讓步,能得到不錯的平衡

Slide 86

Slide 86 text

從另一個角度來看,開發初期加入 Domain-Driven Design 似乎會 多耗費一點開發的時間,然而隨著架構完善實作的速度反而能加快

Slide 87

Slide 87 text

開發初期就開始導入跟敏捷並不太衝突,需要的是視情況而定做一 些取捨跟調整,反覆檢查是否有偏離即可

Slide 88

Slide 88 text

Blog https://blog.aotoki.me 歡迎關注我的部落格,目前週更的主題都圍繞在今天的分享內容上