耦合是什麼#

「耦合(coupling)」常被當成爛設計的代名詞,於是工程師的直覺就是「全部解耦」——拆分類別、模組、服務或整個系統,期望換來獨立演進的自由。但這真的是對的方向嗎?

從詞源來看,coupling 源自拉丁文 copulare(co-「一起」+ apere-「繫緊」),意思就是「繫在一起」、「連結」。

Figure 1.1: 「coupling」一詞的詞源

看到「coupled」就把它替換成「connected」。「兩個服務是耦合的」等於「兩個服務是連結的」。

耦合無所不在:齒輪與彈簧連結成鐘錶、引擎與輪軸連結成車輛、器官連結成生物、粒子連結成宇宙萬物,連天體之間都因引力而保持耦合。

耦合的強度(Magnitude)#

耦合的強度反映「相連元件之間的相互依賴程度」:連結越強,維護所需的成本越高。即便所謂「鬆耦合」,元件之間仍然彼此影響,無法完全獨立。

在軟體設計中,耦合強度越高,元件越常需要「一起被修改」。造成這種「一起改」的根源有兩個:共享生命週期(shared lifecycle)共享知識(shared knowledge)

共享生命週期#

最直接的耦合就是把元件綁在同一個生命週期裡。例如同一個 PaymentsAuthorization 模組:

  • 共置於 monolith 中:必須一起測試、一起部署、一起維護,生命週期耦合度高
  • 抽出成兩個獨立服務(如 BillingIdentity & Access:各自可以獨立開發與部署,生命週期耦合度降低

Figure 1.2: 將模組共置於同一封裝邊界會增加生命週期耦合

除了封裝邊界(encapsulation boundary)之外,還有許多結構性與組織性因素會耦合元件的生命週期,這些會在 Chapter 8(距離)中深入討論。

共享知識#

要協同工作,耦合元件必然得共享某些知識。共享越多,連動修改越多。書中以 CustomersService 模組依賴 repository 為例:

  • MySQLRepository:名稱直接洩漏「使用 MySQL」這個實作細節,要換成記憶體版本就得改 CustomersService
  • 依賴 IRepository 介面(暴露 BeginTransactionExecuteSQL:封裝了「具體是 MySQL」這件事,但仍洩漏「屬於關聯式資料庫」這個事實,換成 key-value 資料庫照樣要改
  • 依賴 IRepository 介面(暴露 SaveQuery:連「關聯式資料庫」都封裝起來,可換的範圍大幅擴張

Figure 1.3: 不同設計共享的知識量不同

共享知識可以是隱性的(implicit)。元件可能對作業系統版本、特定硬體做出未明說的假設,這些都是潛在的耦合點。

知識的流向(Flow of Knowledge)#

為了討論方便,作者建立一套貫穿全書的詞彙,描述知識在元件間的流動方向。

考慮 Distribution 元件依賴 CRM 元件的情境:

  • Distribution 必須了解 CRM 的整合介面、功能與運作細節
  • 也就是說,知識的流向跟依賴方向相反:依賴指向 CRM,但知識從 CRM 流向 Distribution

書中以「上游/下游」描述這個流動:

  • 上游元件(upstream):提供功能給其他元件使用,介面對外暴露其知識
  • 下游元件(downstream):消費上游元件提供的功能,必須吸收上游介面所共享的知識

在上述例子中,CRM 是上游、Distribution 是下游(或者說 DistributionCRM 的消費者)。

Figure 1.4: 知識在耦合元件之間的流向

系統的構成#

要理解耦合在系統中的角色,得先定義「系統」。Donella H. Meadows 在 Thinking in Systems: A Primer 中的經典定義是:

系統是一組互相連結的元素,被組織起來以達成某個目的。

這個定義點出系統的三個核心構成:元件(components)連結(interconnections)目的(purpose)

軟體本身就是「由系統組成的系統」:

  • 巨觀層級:服務、應用、排程作業、資料庫等元件耦合起來,達成業務功能
  • 中觀層級:每個服務都是系統,由實作功能的類別組成
  • 微觀層級:類別本身也是系統,由方法與變數組成;甚至一個方法都可以視為由語句組成的系統

Figure 1.5: 典型軟體系統的組成元件

軟體系統這種層層巢套的「層級結構」是貫穿全書的母題,最終會在 Chapter 12「軟體設計的碎形幾何」中收束。

系統三要素的相互依賴#

齒輪只有放對位置才能讓鐘錶運作。系統的三個要素彼此緊密相連:

Figure 1.6: 鐘錶的組成元件

  • 目的決定需要哪些元件、需要怎樣的互動
  • 元件介面決定哪些整合方式可行,元件功能決定系統能否達成目的
  • 互動透過協調元件,讓系統真正完成目的

改動三要素的任何一個,必然牽動其他至少一個。改變系統的目的,就得修改元件,而元件改變又會引發互動方式的變化。

Figure 1.7: 系統的核心要素彼此互相依賴

這也帶出系統設計的另一個關鍵概念——邊界(boundaries)。引用 Ruth Malan 的話:

System design is inherently about boundaries (what’s in, what’s out, what spans, what moves between) and about tradeoffs.

元件的邊界定義了:

  • 哪些知識屬於這個元件、哪些不屬於
  • 哪些功能由它實作、哪些責任交給其他元件
  • 元件之間如何互動,哪些知識能跨越邊界

為什麼不能完全解耦#

「完全解耦」是個迷思。如果兩個元件需要協同工作,它們就一定要共享知識——互動本身就需要知識共享。沒有互動就沒有耦合,沒有耦合就無法達成系統的目的。

軟體工程師常把焦點放在「拆盒子」——把業務領域分解成服務、模組、物件。但盒子之間的「線」至少同樣重要。設計什麼知識能跨邊界、如何跨邊界、影響為何,才是設計的真正核心。

必要與意外的耦合#

耦合是把系統黏在一起的膠水,但盲目引入依賴並不會產生好設計。耦合分為兩類:

  • 必要的耦合(essential coupling):來自系統目的本身,是不可避免的
  • 意外的耦合(accidental coupling):來自不當設計,可以被消除

模組化設計的工作就是「消除意外耦合,謹慎管理必要的相互依賴」。

機械工程中的耦合(補充)#

機械製造中也有類似概念。製造的接合零件不可能完全精確無誤差,因此設計時會給連接點預留容差(tolerance)

  • 容差過大 → 連接不可靠
  • 容差過小 → 無法吸收製造誤差

Figure 1.8: 透過耦合接點設計連接的兩個零件

軟體設計中的耦合也是同樣道理:「剛剛好」最重要。

重點整理#

評估程式碼時,常問自己這兩個問題:

  • 元件需要知道其他元件的什麼,才能協同工作?這些共享知識若改變,影響範圍有多大?
  • 哪些元件僅僅因為跟易變元件「同生命週期」才被連帶測試與部署?

本章的核心訊息:耦合不該被當成「壞設計」的同義詞,而是設計工具。真正讓系統失控、讓程式碼陷入泥沼的力量另有其名——複雜度(complexity),這是下一章的主題。