本章探討如何在大型系統中聚焦核心問題,避免被大量次要議題淹沒。Distillation(蒸餾)是將混合物分離出精華的過程——在 Domain Model 中,就是區分出真正讓軟體與眾不同、值得投資建構的部分:Core Domain

Strategic Distillation 能達成以下目標:

  1. 幫助團隊掌握系統整體設計及各部分如何協作
  2. 識別出可管理規模的核心模型,納入 Ubiquitous Language
  3. 引導重構方向
  4. 將工作聚焦於模型中最具價值的部分
  5. 指導外包決策、現成元件採用與人員分配

Figure 15.1: A navigation map for strategic distillation

Core Domain#

在大型系統中,有太多不可或缺的元件,真正的業務資產——Domain Model 的精華——容易被遮蔽和忽視。

問題本質#

  • 難以理解的系統就難以變更,變更的影響也難以預見
  • 開發者被迫專精於特定模組,知識傳遞因此受阻
  • 當開發者侷限於各自模組,系統整合品質下降,工作分配彈性也降低
  • 重複程式碼悄悄出現,系統變得更加複雜

最頂尖的開發者往往被吸引去做技術基礎設施或容易定義的 domain 問題,因為這些工作看起來有趣、能建立可轉移的專業技能。而真正區分應用程式、使其成為商業資產的 Core Domain,反而落入經驗較淺的開發者手中。

解決策略#

精煉模型,找出 Core Domain,並提供容易區分它與大量支援性模型和程式碼的手段。 讓最有價值、最專業的概念突顯出來,保持 Core 盡可能小。

關鍵行動:

  • 將頂尖人才分配到 Core Domain,並據此招募
  • 在 Core 中投入精力,尋找 Deep Model 並開發 Supple Design
  • 讓其他部分的投資以「如何支援提煉後的 Core」為依據
  • 需要保密作為競爭優勢的部分就是 Core Domain
  • 當時間有限必須在兩個重構之間抉擇時,優先選擇最影響 Core Domain 的那個

Choosing the Core#

Core Domain 的選擇取決於觀點。例如,多數應用只需通用的貨幣模型,但貨幣交易應用可能需要更精細的貨幣模型作為 Core 的一部分。

  • 一個應用的 Core Domain 可能是另一個應用的 Generic Supporting Component
  • 在同一專案甚至同一公司內,通常能定義出一致的 Core
  • Core Domain 的辨識應隨迭代演進——起初看似核心的物件可能只是支援角色

Who Does the Work?#

  • 技術最強的成員通常對 domain 了解最少,這限制了他們的貢獻並強化了將其分配到支援元件的傾向
  • 必須打破此惡性循環:組建由長期投入的強力開發者深諳業務的 Domain Expert 組成的團隊
  • 短期外部設計專家不適合直接建構 Core Domain(因為團隊需要累積 domain 知識,臨時成員會造成知識流失),但擔任教學/指導角色則非常有價值
  • Core Domain 通常無法購買——即使有產業框架,客製軟體最大價值來自對 Core Domain 的完全掌控

如果一個框架過度約束了你的 Core Domain,可能有三種情況:(1) 你正在失去關鍵軟體資產;(2) 該框架涵蓋的範圍其實不如你想的那麼核心,應重新劃定 Core 邊界;(3) 你的 Core Domain 沒有特殊需求,考慮購買軟體整合即可。

An Escalation of Distillations#

蒸餾技術的激進程度由低到高:

  1. Domain Vision Statement — 以最少投資傳達基本概念與價值
  2. Highlighted Core — 改善溝通並引導決策,幾乎不需修改設計
  3. Generic Subdomains — 透過重構和重新封裝,明確分離通用子領域
  4. Cohesive Mechanisms — 將機制封裝為靈活、富表達力的設計
  5. Segregated Core — 在程式碼中直接呈現 Core,促進未來對 Core Model 的工作
  6. Abstract Core — 以最純粹的形式表達最基本的概念與關係(需要大幅重組與重構)

每一層蒸餾都需要更大的承諾,但刀刃磨得越細越鋒利。

flowchart BT
    L1["1. Domain Vision Statement\n最少投資"] --> L2["2. Highlighted Core\n幾乎不改設計"]
    L2 --> L3["3. Generic Subdomains\n重構分離"]
    L3 --> L4["4. Cohesive Mechanisms\n封裝機制"]
    L4 --> L5["5. Segregated Core\n程式碼層級分離"]
    L5 --> L6["6. Abstract Core\n最純粹形式"]

    InvestLabel["投入遞增 →"] ~~~ L1
    L6 ~~~ EffectLabel["← 效果遞增"]

    style InvestLabel fill:none,stroke:none,color:#666
    style EffectLabel fill:none,stroke:none,color:#666

Generic Subdomains#

模型中有些部分增加複雜度但不傳達專業知識——任何無關的東西都會讓 Core Domain 更難辨識。這些通用元素對系統運作和模型完整表達不可或缺,但它們抽象的概念在許多不同業務中都會用到(如組織架構圖、會計模型等)。

識別出不是專案動機的內聚子領域,將其通用模型抽離到獨立 Modules 中,不留下任何你的專業特色。 分離後,給予它們比 Core Domain 更低的開發優先順序。

四種實作選項#

Option 1: Off-the-Shelf Solution(現成方案)

優點缺點
減少開發量需要時間評估和理解
維護負擔外部化品質無法保證
通常更成熟、更完備可能過度工程化,整合成本高於簡單自製
可能引入平台或編譯器版本依賴

Option 2: Published Design or Model(已發表的設計或模型)

優點缺點
比自製模型更成熟、反映眾多見解可能不完全符合需求或過度工程化
自帶高品質文件
  • 當領域已有高度形式化的嚴謹模型(如會計學、物理學),應直接採用

Option 3: Outsourced Implementation(外包實作)

優點缺點
核心團隊可專注 Core Domain仍需核心團隊投入時間溝通介面和編碼標準
不需永久擴大團隊接收程式碼有額外負擔
強制 interface-oriented 設計
  • 自動化測試在外包中扮演重要角色——實作者應提供 unit test,最理想的做法是由團隊撰寫自動化驗收測試

Option 4: In-House Implementation(自行實作)

優點缺點
容易整合持續維護和培訓負擔
量身訂做容易低估開發時間和成本
可指派臨時承包人

範例:A Tale of Two Time Zones#

兩個專案都花費頂尖開發者數週時間處理時區問題,形成鮮明對比:

  • 貨運排程專案:Core Domain 已成熟,確認需要時區功能後才指派短期承包開發者進行研究與實作,最終成功交付
  • 保險理賠專案:Core Domain 尚未開發,就先行指派長期開發者(本應累積 domain 知識的人)投入時區問題,需求不明確下嘗試全面通用化,結果程式碼從未被使用

兩個專案都正確地將 Generic 時區模型與 Core Domain 乾淨地隔離。但保險專案在 Core Model 尚未成熟時就分散注意力,而貨運專案則是在 Core 成熟後才分配資源處理 Generic 問題。

Generic 不代表 Reusable#

  • 分離 Generic Subdomain 時,不應關注程式碼重用性——這違背了 distillation 的動機
  • Model 層級的重用(如採用已發表的設計)往往比 code 層級的重用更有價值
  • 必須嚴格保持在通用概念範圍內——引入產業特定元素會妨礙未來擴展,且那些專業概念應放在 Core Domain 或更專門的 subdomain 中

Project Risk Management#

  • Agile 流程強調先處理高風險任務,但小心不要因為 Generic Subdomain 比較容易分析就先建構它
  • 除非團隊技能已證明且 domain 非常熟悉,第一版系統應基於 Core Domain 的某個部分
  • Core Domain 是高風險的:經常比預期困難,且沒有它專案不可能成功

Domain Vision Statement#

在專案初期模型尚未存在時,就需要聚焦開發方向。在後期,需要一種不需深入研究模型就能解釋系統價值的方式。此外,Core Domain Model 的關鍵面向可能跨越多個 Bounded Contexts。

撰寫一份簡短的描述(約一頁),闡述 Core Domain 及其帶來的價值(即「value proposition」)。忽略那些無法區分此 Domain Model 與其他模型的面向。展示 Domain Model 如何服務並平衡不同利益。保持精簡。儘早撰寫並隨著新洞察持續修訂。

Domain Vision Statement 的用途:

  • 作為指引,讓開發團隊在持續蒸餾模型與程式碼的過程中保持共同方向
  • 可與非技術團隊成員、管理層甚至客戶分享

Domain Vision Statement 應聚焦於 domain model 的本質及其對企業的價值,而非技術架構、UI 設計或效能需求等面向。例如,航空訂位系統的 Vision Statement 應描述旅客優先順序模型和航空公司訂位策略,而非 XML 介面設計或 logo 快取策略。

Highlighted Core#

Domain Vision Statement 以宏觀方式辨識 Core Domain,但將具體 Core 模型元素的辨識留給個人詮釋。除非團隊溝通品質極高,否則單靠 Vision Statement 影響有限。

不同的人不會選出完全相同的元素,甚至同一人在不同天的判斷也不一致。持續篩選模型以辨識關鍵部分會消耗本可用於設計思考的專注力。

The Distillation Document#

撰寫一份極簡短的文件(三到七頁稀疏內容),描述 Core Domain 及 Core 元素之間的主要互動。

常見風險及應對:

  1. 文件可能未被維護 — 保持絕對精簡,聚焦於中心抽象及其互動(這層模型通常更穩定)
  2. 文件可能未被閱讀 — 為非技術成員撰寫,作為所有人探索模型和程式碼的起點
  3. 多重資訊來源可能反而增加複雜度 — 遠離瑣碎細節

The Flagged Core#

在模型的主要儲存庫中標記 Core Domain 的元素,不特別闡述其角色,讓開發者能毫不費力地知道什麼在 Core 內、什麼在 Core 外。

具體手段可以是:

  • UML 圖中使用 stereotype 標識 Core 元素
  • 程式碼中使用註解(如 JavaDoc)
  • 開發環境中使用某種工具
  • 紙本文件上的頁籤和螢光筆標記

The Distillation Document as Process Tool#

Distillation Document 可作為流程工具:

  • 當開發者發現其程式碼或模型變更需要同步修改 Distillation Document 時,就需要與團隊諮商
  • 這代表他們正在根本性地改變 Core Domain 元素或關係,或改變 Core 的邊界
  • Core 外的變更或未包含在文件中的細節變更,可在不諮商或通知的情況下整合

Cohesive Mechanisms#

封裝機制是物件導向設計的標準原則——將複雜演算法隱藏在具有 Intention-Revealing Name 的方法中,分離「what」與「how」。但當計算複雜度膨脹到一定程度,概念性的「what」會被機制性的「how」淹沒。

將概念上內聚的機制分離到獨立的輕量框架中。 特別注意已形式化或有良好文件記載的演算法類別。透過 Intention-Revealing Interface 暴露框架能力,讓 domain 的其他元素專注於表達問題(what),將解決方案的複雜性(how)委託給框架。

職責分離原則:

  • Model(Core Domain 或 Generic Subdomain):表述事實、規則或問題
  • Cohesive Mechanism:依模型的規範解決規則或完成計算

範例:組織架構圖中的 Mechanism#

一個需要精細組織架構模型的專案,大部分複雜度涉及遍歷組織樹的特定分支(搜尋特定人員或關係)。團隊認識到這正是 Graph(圖論)形式化能解決的問題。

  • 一位承包商實作了 graph traversal 框架作為 Cohesive Mechanism
  • 組織模型可簡單地用標準圖論術語宣告:每個人是 node,人與人之間的關係是 edge
  • 如果機制被嵌入 domain model 中,模型會與特定解法耦合,且組織模型會變得複雜混亂
  • 保持機制與模型分離允許以 declarative style 描述組織結構

Generic Subdomain vs. Cohesive Mechanism#

Generic SubdomainCohesive Mechanism
本質基於表達性模型,代表團隊對 domain 某方面的理解不代表 domain,而是解決表達性模型提出的棘手計算問題
與 Core 的關係與 Core Domain 性質相同,只是較不核心、較不重要為模型服務的計算引擎

「A model proposes; a Cohesive Mechanism disposes.」(模型提出問題;機制處理問題。)在實務中,除非識別出已形式化的計算,這個區分通常不會一開始就很純粹。透過連續重構,它可能被蒸餾為更純粹的機制,或轉化為一個帶有先前未識別的模型概念的 Generic Subdomain。

When a Mechanism Is Part of the Core Domain#

幾乎總是應該將 Mechanism 從 Core Domain 中移除。唯一例外是機制本身是專有的且是軟體價值的關鍵部分——例如物流應用中特別有效的排程演算法,或投資銀行中高度專有的風險評估演算法。

即使在這些情況下,更深入的分析可能揭示演算法只是一組規則的特定實作,這些規則可以被明確表達並配上封裝的求解機制。

Distilling to a Declarative Style#

當 Cohesive Mechanisms 透過 Intention-Revealing Interface 提供存取,配合概念上一致的 Assertions 和 Side-Effect-Free Functions,Core Domain 就能做出有意義的陳述,而非呼叫晦澀的函式。當 Core Domain 的一部分突破到 Deep Model 並開始作為一種語言運作時,就能靈活、精煉地表達最重要的應用場景。

Segregated Core#

即使抽離了 Generic Subdomains,Core 元素仍可能與通用元素緊密耦合,概念內聚性不夠強或不夠明顯。所有這些混亂和糾纏會扼殺 Core。

重構模型,將 Core 概念從支援角色(包括定義不清的部分)中分離出來,強化 Core 的內聚性同時減少它與其他程式碼的耦合。將所有通用或支援性元素重構到其他物件和其他 packages 中,即使這意味著拆開高度耦合的元素。

重構步驟#

  1. 辨識一個 Core subdomain(可能從 Distillation Document 出發)
  2. 將相關類別移到以概念命名的新 Module
  3. 重構程式碼,切斷不直接表達該概念的資料和功能,放到其他 packages 的(可能是新的)類別中
  4. 重構新分離的 Core Module,使其關係和互動更簡單、更具溝通性,並最小化與其他 Modules 的關係
  5. 對其他 Core subdomain 重複以上步驟,直到 Segregated Core 完成

The Costs of Creating a Segregated Core#

  • 分離 Core 有時會使與緊密耦合的非 Core 類別的關係更晦澀或更複雜——但這被釐清 Core Domain 的好處所抵消
  • 為了凸顯 Core Domain 的內聚性,可能需要打破某個原本內聚良好的 Module——這是淨正收益
  • 分離 Core 需要大量工作,可能需要開發者在整個系統中進行變更
  • 最適合進行 Segregated Core 的時機:有一個對系統至關重要的大型 Bounded Context,但其核心部分被大量支援功能遮蔽

Evolving Team Decision#

整個團隊必須一起邁向 Segregated Core。此決策需要團隊級的流程和紀律:

  • 約束每個人使用相同的 Core 定義,同時不凍結該定義
  • 新洞察必須持續與團隊分享,但個人不能單方面行動
  • 無論決策流程是共識制還是團隊領導指令制,都必須足夠敏捷以進行反覆的路線修正

範例:Segregating the CORE of a Cargo Shipping Model#

Figure 15.2: A model for cargo shipping coordination

以貨運協調軟體為例,從 Domain Vision Statement 出發:

"…Increase visibility of operations and provide tools to fulfill customer requirements faster and more reliably…"

此應用是為第一線操作人員設計的,因此所有與金錢相關的議題(pricing、invoices)都退居支援角色。焦點應放在貨物處理:依據客戶要求交付貨物

分離後的關鍵洞察:

  • Customer Agreement 現在直接約束 Handling Step(分離 Core 時典型的洞察浮現)
  • Customer Agreement 直接附加到 Cargo 而非需要透過 Customer 導航——在實際交付時,Customer 不如 Agreement 本身與作業相關
  • Customer 被拉出 Core——基本的 Customer 模型相當通用,有了直接可用的 Customer Agreement 後,交付互動通常不需要 Customer 類別

Figure 15.3: Reliable delivery in adherence with customer requirements is the core goal of this project.

完成 Segregated Core 後,可以接著對剩餘部分進行更有意義的封裝重構:

Figure 15.4: Meaningful MODULES for non-CORE subdomains follow after the SEGREGATED CORE is complete.

最終形成:一個 Segregated Core package、一個 Generic Subdomain、以及兩個扮演支援角色的 domain-specific packages。

Abstract Core#

即使是 Core Domain Model 通常也有太多細節,難以傳達全貌。我們通常透過將大模型拆解為更窄的 subdomains 並放入獨立 Modules 來管理。但這種分割有時會遮蔽甚至使 subdomains 之間的互動變得更複雜。

識別模型中最基本的概念,將其抽取為獨立的類別、抽象類別或介面。設計此抽象模型以表達重要元件之間的大部分互動。將此抽象整體模型放入獨立 Module,而專門化的詳細實作類別留在各自的 subdomain Modules 中。

關鍵要點:

  • 這不是技術技巧——只有當 polymorphic interfaces 對應到 domain 中的基本概念時才有價值
  • 抽取 Abstract Core 的過程不是機械性的,需要對關鍵概念及其在系統主要互動中的角色有深入理解
  • Abstract Core 最終應類似 Distillation Document,但以程式碼撰寫,因此更嚴謹、更完整

考慮水平切割而非垂直切割。多型(Polymorphism)讓我們能忽略抽象型別實例間的大量細節差異。如果跨 Modules 的大部分互動都能在 polymorphic interfaces 層級表達,就值得將這些型別重構到一個特別的 Core Module 中。

Deep Models Distill#

Distillation 不僅在粗粒度層面將 domain 的部分從 Core 中分離,也意味著持續精煉那些 subdomains(特別是 Core Domain),透過不斷的 refactoring toward deeper insight,驅向 Deep Model 和 Supple Design。

  • 目標是讓模型顯而易見的設計——能簡單地表達 domain
  • Deep Model 將 domain 最本質的面向蒸餾為可組合解決重要問題的簡單元素
  • 雖然 Deep Model 的突破在任何地方都有價值,但在 Core Domain 中它能改變整個專案的軌跡

Choosing Refactoring Targets#

面對分割不良的大型系統,Evans 不贊同兩種常見做法:

  1. 「到處開始,反正全都要重構」 — 除非團隊全是頂尖程式設計師,否則不切實際
  2. 「哪裡痛就重構哪裡」 — 容易在邊緣修修補補、治標不治本,迴避最嚴重的糾結

正確的策略:

  • 在 pain-driven refactoring 中,先看根源是否涉及 Core Domain 或 Core 與支援元素的關係——如果是,優先修復
  • 在有餘裕自由重構時,優先改善 Core Domain 的分割、強化 Core 的隔離、並將支援性 subdomain 純化為 Generic
  • 這是讓重構投資獲得最大回報的方式