為什麼要談「距離」#

Chapter 5–7 圍繞著一個核心觀念:耦合越強,元件越容易需要一起改。但 Chapter 7 結尾留下伏筆:「知識共享不是引發連動修改的唯一原因」。

本章把焦點轉到耦合的另一個維度——空間(space)。元件在程式碼庫中的物理位置如何影響耦合?又如何影響連動修改與系統複雜度?

距離與封裝邊界#

軟體並非生來就有結構。早期程式可以是一大段 statements 加上滿地的 goto。隨著典範演進,我們累積了不同層級的封裝邊界:routine、function、object、namespace/package、library、service……這些邊界其實全都是「軟體模組」(Chapter 4)。

同樣的知識可以被共享在不同物理距離上。把「什麼是 preferred customer」分散在同一物件的兩個方法(A),和分散在兩個微服務(B),共享的是同樣的知識,但維護成本天差地別。

Figure 8.1: 同樣的知識共享於不同的封裝邊界(同物件方法 vs 微服務)

距離的成本#

跨方法 → 跨物件 → 跨 namespace → 跨 library → 跨 service → 跨系統。距離越遠,協同實作變更所需的成本越高。

Figure 8.2: 協調成本隨物理距離變化的趨勢

成本來源包括:

  • 在不同 codebase 中協調修改
  • 跨團隊的溝通與協作
  • 維護者的認知負擔——距離越遠,越容易忘了同步某個元件,造成系統行為錯誤

回到 preferred customer 的例子:

  • A:邏輯重複在同一物件裡:改一處、部署一次
  • B:邏輯散在兩個微服務:兩個服務都要改,且必須同步部署,否則出現不一致

連動修改的成本與距離成正比。

距離 = 生命週期耦合的反向#

Figure 8.3: 生命週期耦合隨物理距離變化的趨勢

軟體模組的生命週期:需求 → 設計 → 實作 → 測試 → 部署 → 維護。生命週期耦合(lifecycle coupling) 指模組必須一起經歷這些階段。

距離與生命週期耦合成反比:距離越近,生命週期耦合越強——即使元件之間根本沒有關聯。

例子:違反 SRP 的 SupportCase#

class SupportCase {
    fun createCase(...) { ... }
    fun assignAgent(...) { ... }
    fun resolveCase(...) { ... }
    fun logActivity(...) { ... }
    fun scheduleFollowUp(...) { ... }

    fun sendEmailNotification(...) { ... }
    fun sendSMSNotification(...) { ... }

    fun processPayment(...) { ... }

    fun convertMilesToKilometers(...): Double { ... }
}

把不相關的功能塞進同一物件,等於把它們的生命週期綁在一起。

想部署支援案件的修改?必須把通知、付款、單位轉換的修改一起 compile/test/deploy,或回滾未完成的部分。或在不同分支開發——但同檔案多分支幾乎注定 merge conflict。

把這個物件拆成 SupportCaseNotificationPaymentUnitConversion 四個物件,即使在同一 namespace 中,至少不必修改同一個檔案。再進一步拆到四個微服務,生命週期耦合會降到最低。

評估距離#

距離可以用「兩個模組最近的共同祖先」來表達。例如以下三個 C# 型別:

1. WolfDesk.Routing.Agents.Competencies.Evaluation
2. WolfDesk.CaseManagement.SupportCase.Message
3. WolfDesk.CaseManagement.SupportCase.Attachment
  • 1 與 2 的共同祖先是 WolfDesk(最頂層)→ 距離大
  • 2 與 3 的共同祖先是 SupportCase → 距離小

影響距離的其他因素#

Figure 8.4: 耦合元件之間距離造成的雙重效應

社會技術設計(Socio-Technical Design)#

距離不只跟程式碼物理位置有關,也跟「擁有權」有關。模組可能由:

  • 同一個人實作
  • 同一團隊
  • 同部門不同團隊
  • 不同部門
  • 甚至不同組織

擁有權距離越遠,協調成本越高。 即使物理上很近,但分屬不同團隊就等於有效距離拉遠。

回到先前微服務的例子:「一起部署」對同團隊只是內部協調;對跨團隊就是大型協作。

擁有權距離也會反向降低生命週期耦合——分屬不同團隊的微服務比較不可能有同一份開發與部署排程。

Conway’s Law#

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.(Melvin Conway, 1968)

軟體系統的結構終將反映其組織的溝通結構。即使初始設計刻意違反組織結構,時間久了也會被「拉回」與組織結構對齊。

擁有權距離會在時間中逐步影響系統的封裝距離。設計時應認真考慮 Conway’s Law,否則組織結構會幫你重新設計系統。

Runtime Coupling(執行期耦合)#

Figure 8.5: 服務之間的同步與非同步整合

執行期耦合指「一個模組的可用性受另一個模組可用性影響的程度」。

  • 同步整合(如 RPC):消費者期待立即回應,提供者掛了 → 消費者直接受影響 → runtime coupling 高
  • 非同步整合(如 message bus):只要消費者能從 message bus 取到訊息,提供者掛了它仍能繼續運作 → runtime coupling 低

高 runtime coupling 會讓故障在元件間傳播,等於把生命週期綁在一起,降低了距離

非同步通訊與變更成本#

「非同步整合通常是更彈性的設計」聽起來很合理。但若兩服務透過 message 整合卻是 model-coupled(訊息直接暴露 producer 內部模型),則任何模型變動都需要 producer 改格式、consumer 改解析——甚至需要中介版本支援新舊雙格式。這時非同步反而比同步更難改。

Distance vs Proximity#

「proximity」是「distance」的反面。兩者是同一現象的不同視角。本書統一用 distance,理由:

  • 「low/high distance」比「low/high proximity」更直觀
  • 距離直接對應變更成本,視覺化容易
  • proximity 傳統上只描述封裝邊界距離,但本書要納入 socio-technical 與 runtime coupling 的影響

Distance vs Integration Strength#

兩者是不同維度:

  • Integration strength:跨邊界共享的知識——決定是否會一起變動
  • Distance:物理 / 組織 / 執行期距離——決定一起變動的成本有多高

常見的設計陷阱:

  • 把 monolith 拆成微服務時只考慮「距離」(封裝邊界),忽略「強度」(知識流向)→ 結果是 distributed Big Ball of Mud
  • 以為導入 EDA 與 message bus 就自動解耦——若知識共享沒被優化,非同步元件仍有共享的變更原因,仍可能引發複雜互動

重點整理#

設計時除了問「共享了哪些知識」,也要問:

  • 這些知識會穿越多遠的距離?(封裝邊界、團隊邊界、執行期邊界)
  • 同處一地的元件即使不共享知識,也會因生命週期耦合而牽動彼此
  • 拆解成本與整合成本要一起評估

下一章將迎來 Part II 最後一個維度:易變性(volatility)——時間維度的耦合。