為什麼還需要新模型#

Chapter 6 結尾留下了一個矛盾:用反射讀取私有欄位的整合,在 module coupling 是最強的 content coupling,在 connascence 卻只是最弱的 connascence of name。這不是錯誤,而是兩個模型描述的是耦合的不同面向

本章把兩個模型的精華整合成一個更實用的工具:整合強度(integration strength)

本章與後續章節中的「interface」一詞,指模組的整合介面,而不是 Java/C# 中的 interface 關鍵字。

耦合的兩個關鍵屬性#

跨模組互動有兩個必須同時考量的屬性:

  • 介面類型(interface type):用什麼樣的介面整合?私有介面(content)?公開介面(stamp、data)?甚至完全沒有實體連線?
  • 介面複雜度(interface complexity):透過介面共享的知識有多複雜?越複雜越容易產生連動修改與整合錯誤

光用 module coupling 評估會看不到細緻度;光用 connascence 評估會看不到介面類型。兩者必須一起用。

兩模型的盲點#

重複實作的隱形耦合#

Figure 7.1: 兩個服務各自實作同一條業務規則

考慮兩個服務 RetailFulfillment 各自實作同一條業務規則「客戶是否合資格享免運」。

  • 兩者並沒有實體連線
  • 卻有極強的「共享變更原因」——任何規則變動都得同時改兩處
  • 不同步的話,會出現「下單時符合、出貨時被收費」的不一致

Module coupling 與 connascence 都無法描述這種耦合。它們只在「實體連線」存在時才適用。

這也不是 connascence of algorithm——後者要求「雙方需用同一演算法解讀資料」,而這裡的重複邏輯與資料交換無關。

Integration Strength 的目標#

新模型要達成三個目標:

  • 實用(Practicality):日常工作中容易理解與套用
  • 通用(Versatility):適用於從程式碼中個別語句到分散式系統間的服務
  • 完整(Completeness):補足 module coupling 與 connascence 的盲點

模型由四個基本層級組成(由強到弱):

  1. Intrusive coupling(侵入式耦合)
  2. Functional coupling(功能耦合)
  3. Model coupling(模型耦合)
  4. Contract coupling(契約耦合)

四個層級反映「介面類型」;其中後三者還有一個額外維度——degree(程度)——用 connascence 評估。

貫穿範例:兩個服務共用資料庫#

Figure 7.2: 兩個服務共用同一個資料庫的陷阱問題

「兩個服務共用資料庫,到底算強耦合還是弱耦合?」——這是一個故意的陷阱問題。光看圖無法判斷,必須看更多細節。後面四個層級都會回到這個範例做對照。

Intrusive Coupling(侵入式耦合)#

Figure 7.3: 侵入式耦合的概念示意

下游模組透過上游的實作細節整合,而非透過為整合設計的公開介面。

Intrusive coupling 等同於 structured design 的 content coupling。書中改名是因為「content」原意是程式碼語句的內容,現代有更多種形式的實作細節依賴。「Intrusive」由 Vaughn Vernon 命名,更能反映「越界」的本質。

例子#

  • 用反射呼叫私有方法、讀取私有欄位
  • 改動商用 off-the-shelf 系統的原始碼塞自己想要的通知機制(升級時整個被覆蓋)
  • 微服務直接讀另一個微服務的資料庫——若該資料庫從未被設計為整合介面

ORM 框架用反射存取物件並不算侵入式耦合——使用 ORM 時你已明確同意它存取物件欄位。差異在於「上游是否知情並允許」。

範例(共用資料庫)#

Figure 7.4: 存取非為整合而設計的資料庫導致侵入式耦合

服務 B 直接讀服務 A 的資料庫,且該資料庫從未被設計成整合介面 → 侵入式耦合。

為何位於最頂層#

  • 脆弱:上游任何修改都可能破壞整合
  • 隱晦:上游甚至不知道有人依賴它,連審查破壞性變更的機會都沒有
  • 破壞封裝:必須假設上游所有實作細節都已對下游公開

Functional Coupling(功能耦合)#

兩個模組的功能互相關聯——透過共享業務規則、不變量、演算法等業務邏輯耦合在一起。

功能耦合沒有上下游之分:知識雙向流動,任一邊修改都可能波及對方。

三種程度#

由弱到強:

  • Sequential functionality(順序功能)
    • 多個動作必須以特定順序執行
    • 對應 connascence of execution / connascence of timing
  • Transactional functionality(交易功能)
    • 多個操作必須作為單一工作單元成功或失敗,需要並發控制
    • 對應 connascence of value(值需一起變動)與 connascence of identity(共享資料的強一致性)
  • Symmetric functionality(對稱功能):最強的功能耦合
    • 不同模組實作同一份功能
    • 對應違反 DRY 原則:「Every piece of knowledge must have a single, unambiguous, authoritative representation within a system」
    • 即便不同實作方式也算(重點是「同一個業務知識」)
    • 因為高度隱晦(兩邊可能完全不知道對方存在)卻必須同時改,所以僅次於 intrusive coupling

Figure 7.5: 功能耦合的三種程度

識別功能耦合的捷思:「如果業務需求變了 X,會不會有多個模組都得改?」會 → 功能耦合。

範例(共用資料庫)#

Figure 7.6: 兩個服務操作同一份資料導致功能耦合

兩個服務都讀寫同一張資料表,且都需遵守同一份業務規則維持資料一致性 → 功能耦合,程度為 connascence of identity。

Model Coupling(模型耦合)#

多個模組共用同一個業務領域模型

模型不是業務領域的完整重現,只是「為了實作某項功能而抽取的子集」。「All models are wrong, but some are useful.」(George Box)。

不同的業務功能往往需要不同的模型。DDD 之所以強調為不同功能設計多個模型,正是這個道理。

為何模型不該被任意共享#

Figure 7.7: 上游模組將內部模型當作公開介面暴露

範例:Distribution 模組把內部模型當公開介面對外暴露,Accounting 模組重用了它的部分模型。

  • Accounting 可能本來需要不同的模型來表達自己的業務
  • 一旦模型變動,連動修改就會跨模組擴散
  • 限制了 Distribution 自己模型的演進能力

此層級只關心「資料模型」共享。共享行為屬於 functional coupling。

Degree#

Figure 7.8: 模型耦合的程度

用靜態 connascence 五個層級評估(name、type、meaning、algorithm、position)。

範例(共用資料庫)#

Figure 7.9: 讀取另一元件的營運資料庫導致模型耦合

服務 A 擁有資料庫並負責管理資料,但允許服務 B 唯讀。資料庫此時就是公開介面,但讀到的是服務 A 的內部模型 → 模型耦合。

為何比侵入式與功能耦合好#

  • 暴露的是資料而非行為——資料結構通常比行為穩定
  • 整合介面顯式——知道哪些模型被共享,可記錄、可識別破壞性變更
  • 上游知情——可以採取措施避免破壞性變更

但仍可能引入意外性複雜度(暴露不必要欄位 = stamp coupling 的延伸)。

Contract Coupling(契約耦合)#

Figure 7.10: 透過為整合最佳化的模型連接下游元件

模組之間透過整合專用模型(integration-specific model,即「contract」) 通訊。

Contract 是「模型的模型」——把業務領域模型中與整合無關的部分抽掉,暴露出最小必要知識。

引入整合契約的好處#

  • 比實作模型更穩定:實作可以演進,只要還能對應到同一份契約,就不會波及下游
  • 最小化共享知識:契約以外的內部模型完全封裝
  • 可版本化:同一上游模組可同時暴露多個版本契約,方便下游漸進遷移

範例:把領域物件抽成 DTO#

把內部 SupportCase(含 CaseIdAgentIdCustomerId 等領域物件)轉成只用 primitive 型別的 SupportCaseDetails,方便跨平台。

範例:用 Command / Query 構造任務導向介面#

namespace WolfDesk.SupportCases.Application.API.Commands {
    class EscalateCase(val caseId: String, val customerId: String, val escalationReason: String)
    class ResolveCase(val caseId: String, val comment: String)
    class PutCaseOnHold(val caseId: String, val comment: String, val until: Long)
}

interface SupportCasesAPI {
    fun execute(cmd: EscalateCase): ExecutionResult
    fun execute(cmd: ResolveCase): ExecutionResult
    fun execute(cmd: PutCaseOnHold): ExecutionResult

    fun casesByAgent(agentId: String): List<SupportCaseDetails>
    fun casesByCustomer(customerId: String): List<SupportCaseDetails>
    fun escalatedCases(): List<SupportCaseDetails>
}

Contract coupling 不只在分散式系統。Facade、Bridge、Adapter 等設計模式本質上都是某種形式的整合契約。SMTP/IMAP/POP3 也是現成的整合契約。

Degree#

Figure 7.11: 契約耦合與模型耦合的程度對照

跟 model coupling 一樣,用靜態 connascence 評估。

即便 contract coupling 中最強的程度,也比 model coupling 中最弱的程度更弱。差別在於「共享的知識類型」:模型是實作細節,契約不是。

Contract 的「深度」#

Figure 7.12: 評估模組的視覺啟發 — 深度

// Operational model
class Message(val id: UUID, val body: String, val sentOn: DateTime, val sentBy: UUID)

// "Contract",但欄位完全相同 → 沒封裝任何東西
class MessageDTO(val id: UUID, val body: String, val sentOn: DateTime, val sentBy: UUID)

這種 DTO 是個「淺模組」——沒封裝任何知識,反而增加了不必要的「moving part」。雖然能讓 MessageMessageDTO 各自演進,但鏡像結構暗示變動仍會跟著傳導。

寧可先用 model coupling,等真正有封裝價值時再引入契約物件。

範例(共用資料庫)#

Figure 7.13: 透過暴露含整合專用資料的資料庫 schema 形成契約耦合

資料庫中有一張專為整合設計的表,schema 與服務 A 的實作模型解耦 → contract coupling。

軟體設計中常用「semantic coupling」來涵蓋 model coupling 與 contract coupling,但本書刻意分開——兩者整合強度差距太大,混為一談會抹殺重要差異。

Integration Strength 與兩前模型的對照#

Figure 7.14: 共享變更原因隨介面類型與複雜度而變化

Integration StrengthModule Coupling用途
IntrusiveContent上游沒打算被當整合介面
FunctionalCommon / External / Control行為層面的依賴
ModelStamp共享了多餘資料結構
ContractData介面為整合而設計,最小化共享

整合強度 = 介面類型(四個基本層級) × 介面複雜度(degree 用 connascence 衡量)

範例:分散式系統#

Figure 7.15: 分散式系統的範例

考慮一個分散式系統:

  • 服務 A 接受使用者命令 → 寫入資料庫 + 發訊息到 message bus
  • 服務 B:把訊息存進自己的資料庫供稽核
  • 服務 C:用訊息更新自身狀態
  • 服務 D:用服務 C 提供的狀態 enrich 訊息後,送進分析資料庫;訊息延遲 30 秒投遞,等服務 C 處理完

可以辨識出:

  • 資料庫 + message bus 必須交易性更新 → functional coupling,degree = connascence of value
  • 服務 C 用了服務 A 公開的領域模型 → model coupling
  • 服務 D 必須等服務 C 處理完才能跑(30 秒延遲)→ functional coupling,degree = connascence of timing

同步 vs 非同步#

Figure 7.16: 同步與非同步整合的元件

業界常認為非同步比同步耦合弱。本章直接挑戰這個觀念。

同步或非同步通訊本身並不影響整合強度。 它影響的是其他面向,將在 Chapter 8(距離)中討論。

例如同樣用 message queue 通訊,可能對應到:

  • 訊息是整合專用模型 → contract coupling
  • 訊息直接是業務領域模型 → model coupling
  • 訊息有延遲投遞語意 → functional coupling(connascence of timing)
  • 兩端必須同時成功或同時失敗(含補償動作)→ functional coupling(connascence of value)
  • 訊息匯流排其實是上游的內部實作 → intrusive coupling

重點整理#

評估某個整合時,問自己兩件事:

  • 我用的是哪一種介面?私有實作?業務功能耦合?業務模型?整合契約?
  • 透過這個介面共享的知識複雜度有多高?(用 connascence 評估)

下一章把焦點從「強度」轉到另一維度:距離(distance)