2024年4月30日
怎麼劃分 Domain?找出子域、分辨核心/支撐/通用,再對應到限界上下文
DDD 概論 裡簡單介紹了子域和限界上下文,Domain Storytelling 那篇也從故事裡找出了「搜尋、帶看、斡旋」三個候選邊界。但真的要替整個系統劃分時,還有幾個問題要回答:
- 子域要怎麼找? 只靠一個故事不夠,整個業務要從哪些角度看?
- 怎麼判斷是核心、支撐還是通用? 每個人都覺得自己負責的是核心
- 子域和限界上下文是一對一嗎? 系統要照子域切嗎?
先分清楚三個名詞
| 名詞 | 屬於 | 說明 | 房仲的例子 |
|---|---|---|---|
| 領域(Domain) | 問題空間 | 公司從事的整個業務 | 房屋仲介 |
| 子域(Subdomain) | 問題空間 | 業務中相對獨立的一塊能力 | 物件刊登、物件媒合、帶看、斡旋成交、佣金業績 |
| 限界上下文(Bounded Context) | 解法空間 | 一個模型與它的語言有效的範圍,是系統的切法 | 刊登上下文、搜尋上下文、成交上下文 |
子域是「發現」的,限界上下文是「設計」的。 子域在寫任何程式之前就存在:就算公司全部用紙本作業,「帶看」這件事還是存在;限界上下文則是我們決定怎麼用軟體去解決它。
第一步:找出子域
沒有一個公式能直接算出子域,但有幾個角度可以交叉比對:
1. 從業務能力出發
問「公司做哪些事?」,用「動詞+名詞」列出業務能力,而不是列出系統功能或資料表:
- 開發委託:說服屋主簽下委託
- 刊登物件:把物件資訊、照片放上網站
- 媒合買方:讓買方找到適合的物件
- 安排帶看:協調買方、屋主、經紀人的時間
- 斡旋議價:在買賣雙方之間協商價格
- 簽約交屋:簽訂買賣契約、交屋
- 計算佣金與業績:服務費怎麼拆給經紀人與分店
2. 從流程中的關鍵事件切
跑過 Event Storming 或畫過粗粒度的領域故事後,時間軸上會有幾個關鍵事件(Pivotal Event),它們是業務階段的分界:
委託已簽訂 → 物件已上架 → 帶看已完成 → 斡旋已接受 → 買賣契約已簽訂 → 已交屋
兩個關鍵事件之間的那一段,通常就是一個子域的候選。
3. 從語言的變化
同一個詞意思開始不一樣的地方,就是邊界:
- 刊登時的「物件」是一則廣告,在意的是照片好不好看、文案吸不吸引人
- 成交時的「物件」是一個交易標的,在意的是權狀、地號、有沒有抵押
4. 從組織與變化頻率
- 誰負責? 刊登歸行銷、斡旋歸店長、佣金歸財務,不同部門的需求很少會同時改到
- 多常改? 搜尋與推薦幾乎每個月都在調整,佣金規則一年才改一次
這四個角度得出的結果通常不會完全一致,不一致的地方就是要拿去和領域專家討論的地方。
第二步:分類
找出子域後,下一步是決定每一塊值得投資多少。分類的關鍵問題只有一個:這一塊做得比對手好,客戶會因此選擇我們嗎?
| 類型 | 判斷問題 | 策略 |
|---|---|---|
| 核心子域(Core) | 做得好會讓客戶選我們、做不好會失去客戶 | 自己做,放最好的人,用完整的 DDD 設計,持續投資 |
| 支撐子域(Supporting) | 業務需要、買不到現成的,但做得再好客戶也不會在意 | 自己做但做簡單,可以外包,不需要複雜的設計 |
| 通用子域(Generic) | 每家公司都有一樣的問題,市場上有成熟方案 | 買現成的、用開源或雲端服務,不要自己寫 |
用 Core Domain Chart 畫出來
只用文字分類容易流於主觀,可以用 Nick Tune 提出的 Core Domain Chart,把每個子域放在兩個維度上:
- 橫軸:模型複雜度,業務規則有多複雜、多難做對
- 縱軸:業務差異化,對競爭優勢的影響有多大
圖的讀法:
- 右上(核心):複雜又能帶來差異,是最值得投資的地方。物件媒合做得好,買方更快找到房子,經紀人的帶看成交率也更高
- 左下(支撐):簡單、不帶來差異,自己做但用最簡單的方式做。帶看排程只要能約時間、會提醒就夠了
- 右下(通用):很複雜,但做得好也不會讓客戶選我們。金流、身分驗證自己寫很容易出錯,應該買現成的
- 左上:簡單但有差異化,可能是一個剛出現的機會,或是很快會被對手模仿的功能
佣金與業績是個有趣的例子:規則其實不簡單(專任/一般委託的比例不同、跨店合作要拆帳),但它不影響客戶的選擇,所以是支撐子域。複雜不等於核心,這是分類時最常見的誤判。
分類不是永久的
- 公司策略決定核心:同樣是房仲,主打「經紀人服務品質」的公司,帶看體驗可能就是核心;主打「線上找房」的公司,核心是搜尋和推薦
- 核心會變成通用:十年前地圖找房是差異化,現在每個房屋網站都有,加上地圖服務越來越成熟,它就變成通用子域了
所以分類的結果要寫下來,並且定期(例如每半年)重新檢視一次。
第三步:對應到限界上下文
理想上,一個子域對應一個限界上下文,邊界最清楚。但實際上常見幾種情況:
- 一個子域、多個上下文:物件媒合包含「搜尋」和「推薦」,兩者的模型不同(搜尋要的是篩選條件,推薦要的是瀏覽紀錄和相似度),可以切成兩個上下文
- 多個子域、一個上下文:舊系統常常一個大系統包辦刊登、帶看、成交。短期內不一定要拆,但要知道它涵蓋了哪些子域,新功能盡量不要再往裡面塞
- 通用子域變成外部服務:金流直接用第三方,在自己這邊加一層防腐層,把對方的格式轉成自己的模型
注意帶看上下文的「物件」只有 Id:帶看只需要知道是哪一間、地址在哪,不需要照片或權狀。每個上下文只保留自己需要的部分,這正是限界上下文的意義。
上下文之間的關係(上下文映射)也要一起決定:
- 刊登 → 搜尋、推薦:刊登上下文發布「物件已上架」、「物件已改價」事件,用統一格式給多個下游(開放主機服務/公開語言),搜尋端再依事件更新自己的索引,做法類似 CQRS 的讀取模型
- 成交 → 外部金流:用防腐層把金流服務的欄位與狀態碼轉成成交上下文的模型,換金流商時只需要改這一層
邊界切錯的徵兆
切完之後,可以用這些問題檢查:
- 一個需求要同時改好幾個上下文? 偶爾發生是正常的,經常發生代表相關的規則被切開了
- 上下文之間一直同步呼叫? 完成一件事要 A 呼叫 B、B 再呼叫 C,任何一個掛掉整個流程就失敗,代表它們其實是同一件事
- 一個上下文需要兩個團隊一起改? 限界上下文最好由一個團隊負責,否則每次修改都要協調
- 同一個上下文裡,同一個詞有兩種意思? 代表裡面其實有兩個模型,應該再切開
幾種常見的錯誤切法:
| 切法 | 問題 |
|---|---|
| 依資料表切:物件服務、會員服務、訂單服務 | 每個業務流程都要跨好幾個服務,「物件服務」又變回所有人共用的大模型 |
| 依技術層切:前端團隊、API 團隊、資料庫團隊 | 任何功能都要三個團隊排程,沒有人對完整的業務負責 |
| 切得太細:每個聚合一個服務 | 服務之間的呼叫和資料同步成本超過拆分的好處 |
| 照組織圖切,但組織本身就不合理 | 康威定律會把組織的問題複製到系統裡,有時候要調整的是團隊分工 |
不同類型,不同做法
分類的目的是把力氣花在對的地方,不同類型的子域,架構和開發方式可以完全不同:
| 核心子域 | 支撐子域 | 通用子域 | |
|---|---|---|---|
| 設計方式 | 完整的戰術設計:聚合、值物件、領域事件 | 簡單的分層或 Transaction Script,CRUD 也可以 | 不自己設計,整合現成方案 |
| 架構 | 六角形架構,隔離業務與技術 | 能動、好維護就好 | 用防腐層包起來 |
| 測試 | 業務規則要有完整的單元測試 | 主要流程的整合測試 | 測試整合的那一層 |
| 人力 | 最資深、最懂業務的人 | 新人、外包都可以 | 少量人力負責整合 |
「全部都用完整的 DDD」和「全部都不用」一樣不好。用在核心子域,才能得到 DDD 最大的回報。
一句話總結
子域是從業務能力、關鍵事件、語言變化與組織分工中「發現」出來的,用「做得好會不會讓客戶選我們」與模型複雜度把它們分成核心、支撐、通用;限界上下文則是我們「設計」的系統切法,理想上和子域一對一,但可以視模型差異再拆或暫時合併,每個上下文只保留自己需要的模型,最後把最多的心力放在核心子域。