從「複雜度是什麼」到「複雜度從哪裡來」#
Chapter 2 用 Cynefin 框架定義了複雜:在複雜領域中,行動的後果只能事後才被辨認,必須靠安全實驗才能揭露因果。但這種不確定性正是軟體設計要避免的——好的設計應該讓變更的後果清晰、可預測。
本章把焦點從「定義複雜」轉向「複雜的成因」,並逐步把複雜度與本書主題——耦合——連結起來。
複雜度的兩種來源#
複雜度有兩個本源:
- 本質性複雜度(essential complexity):源自業務領域本身。例如演算法交易系統必然要面對市場動態、法規、各類金融工具——這些無法被消除,只能透過良好設計(拆成可單獨理解的元件)降低認知負擔
- 意外性複雜度(accidental complexity):來自不良設計決策的副作用,可以也應該被避免
工程師的責任:好好管理本質性複雜度,避免意外性複雜度。
複雜度與系統設計#
Charles Perrow 在 Normal Accidents: Living with High-Risk Technologies 中分析核電廠、空中交通管制等高風險系統的災難性事故,得出結論:所有複雜系統終究會失敗。他把研究焦點放在「系統為什麼會變複雜」,答案歸結為——元件之間的互動是「線性」還是「複雜」。
線性互動(Linear Interactions)#
- 清晰可預測,依賴關係明顯,因果關係明確
- 改一處,能預期會影響到哪些地方
- 例子:機械錶。零件雖多,但每個齒輪如何牽動其他元件都可追蹤

Figure 3.1: 機械錶機芯——線性互動的範例
對應到 Cynefin:線性互動屬於 clear 或 complicated 領域。
複雜互動(Complex Interactions)#
複雜互動既不清晰也不可預測,會帶來兩種惡果。
1. 意圖達成,但方式出乎意料#
「系統能動,但沒人知道為什麼能動」。常見原因:
- 擁有 tacit knowledge 的人離職,剩下的人都看不懂
- 系統充滿意外性複雜度,因為團隊只是跟風引進工具
- 團隊接手了不熟悉技術棧的舊程式碼
演進這種系統極度危險,連微小的修改都可能引發莫名其妙的故障。
2. 非預期結果#
系統雖然完成了目標,卻附帶意外後果或失敗:
- 一個元件的功能變動,連帶影響到看似無關的其他部分
- 一個元件失效,引發連鎖故障
- 對環境做出隱性或錯誤的假設(例如忽略 distributed computing 的 fallacies,假設網路永遠可靠)
- 一個變更必須在多個遙遠元件間協調,很容易遺漏其中一處
非預期結果通常是**隱性知識(implicit knowledge)**的症狀——可能是對環境的假設、也可能是缺乏結構與邊界的程式碼。
複雜度與系統大小無關#
系統的複雜度不是由元件數量決定的。一個 5,000 個元件的系統可能比一個只有 5 個元件的系統還簡單。
真正的關鍵是:元件之間的互動是線性還是複雜。
複雜度的層級結構#
系統是層層巢套的——任何系統都是更大系統的元件(Tim Berners-Lee)。複雜互動可以發生在元件之間,也可以發生在元件之內。
以 WolfDesk 為例:
- WolfDesk 系統 → 由微服務組成
Case Management微服務 → 由物件組成Support Case聚合(aggregate)→ 由實體組成

Figure 3.2: 多層級的複雜度
借用 Glenford J. Myers 的詞彙:
- 全域複雜度(global complexity):系統元件「之間」互動的複雜度
- 局部複雜度(local complexity):單一元件「內部」互動的複雜度
這個分類是相對於觀察視角的。從更高的抽象層級看,今天的全域複雜度就變成局部複雜度。
只優化全域複雜度的陷阱#
把所有元件合併成一個 monolith 可以讓全域複雜度歸零(沒有跨元件互動了),但局部複雜度會爆炸。複雜度只是被藏到更低的抽象層級,並沒有被解決。

Figure 3.3: 將元件合併為 monolith 以降低全域複雜度的天真嘗試
只優化局部複雜度的陷阱#
反向操作同樣失敗。某團隊把 monolith 拆成微服務,並硬性規定每個微服務不超過 100 行程式碼,結果產生了分散式的 Big Ball of Mud——全域複雜度爆表。

Figure 3.4: 只優化局部複雜度來嘗試管理複雜度
這個策略還誤把「複雜度」等同於「大小」。優化「大小」根本不等於管理複雜度,最好的情況是複雜度不變,更可能是引入額外的意外性複雜度。
自由度(Degrees of Freedom)#
「自由度」原本是力學、熱力學的術語,指系統中可獨立變動的變數數量。
軟體設計中的例子:
// 一個自由度
struct Square {
int Edge
}
// 兩個自由度
struct Rectangle {
int Width
int Height
}自由度越多 → 可能狀態越多 → 互動可能性越多 → 越難控制和預測。
自由度氾濫的常見來源#
- 跨資料庫的資料複製:在可用性與資料新鮮度之間取捨。但網路長時間分區會導致
Shipping服務拿到嚴重過期的資料

Figure 3.5: 跨多個儲存機制複製資料
- 重複的業務邏輯:例如
Shipping與Ordering各自實作isQualifiedForFreeShipping,兩邊只要不同步就會出現不一致——客人下單時符合免運條件,但出貨時被收費

Figure 3.6: 兩個服務實作了相同的業務規則
自由度反映了系統可能進入的狀態。多餘的自由度會讓系統有機會進入「不該進入的狀態」,這正是複雜互動的溫床。
複雜度與「約束」(Constraints)#
自由度與 Cynefin 的共通點是「約束」。約束限制了元件互動的可能性,因此降低了自由度。
// 不受約束的三角形:可能存在數學上不合法的狀態
class Triangle {
var EdgeA: Int
var EdgeB: Int
var EdgeC: Int
}
// 透過 SetEdges 強制約束:三邊必須能構成三角形
class TriangleConstrained {
var EdgeA: Int = 0; private set
var EdgeB: Int = 0; private set
var EdgeC: Int = 0; private set
fun setEdges(a: Int, b: Int, c: Int) {
if ((a + b) < c || (a + c) < b || (b + c) < a) {
throw IllegalArgumentException("Invalid triangle")
}
EdgeA = a; EdgeB = b; EdgeC = c
}
}Cynefin 領域與「約束」的對應:
- Clear / Complicated:約束存在且有效,因果緊密耦合
- Complex:約束較少,因果鬆耦合
- Chaotic:完全沒有約束
約束越少,因果關係越不確定,複雜度越高。要馴服複雜互動,就需要約束——而我們手上正有一個能定義「元件如何整合、如何互動」的工具:耦合。
透過耦合設計約束:Repository 範例#

Figure 3.7: Repository 物件旨在封裝實體資料庫
書中以三個 Repository 設計,展示「耦合方式」如何影響複雜度。
Design A:直接吃 SQL Where 子句#
interface SupportCaseRepository {
fun query(sqlWhere: String, params: Map<String, Any>): List<SupportCase>
// ...
}這個設計把大量知識洩漏給呼叫端:
- 資料庫 schema:呼叫端必須知道欄位名與物件屬性的對應
- 資料庫索引:自由查詢容易寫出無索引可用的查詢,引發效能問題
- 資料庫類別與方言:暗示「是 RDB」、「使用某個 SQL 方言」
重新命名
OpenedOn → Timestamp但忘記同步資料表?另一位工程師寫的查詢就會失敗。換到 Cloud Spanner?SQL 方言不同,大量查詢都得改寫。
Design B:用 Query Object#
interface SupportCaseRepository {
fun query(query: QueryObject): List<SupportCase>
// ...
}把 SQL 的翻譯藏進 repository 內部,引入了「不能用 SQL 全部表達能力」的約束。換 DB 家族(如改用 MQL)時更容易。但仍未封裝 schema 細節與索引,仍可能寫出沒有索引可用的查詢。
Design C:專門的 Finder Methods#
interface SupportCaseRepository {
fun allCases(tenantId: TenantId): List<SupportCase>
fun createdBefore(tenantId: TenantId, time: DateTime): List<SupportCase>
fun matchingStatus(tenantId: TenantId, status: Status): List<SupportCase>
// ...
}把欄位名完全封裝、查詢方式列舉清楚,索引設定也容易最佳化。代價是失去 ad hoc 查詢的彈性——這是「彈性」與「降低複雜互動風險」之間的取捨。
耦合、自由度與約束#
從這三種設計可以看出:
- 耦合方式直接決定了元件邊界允許流動的知識(含隱性知識)
- 共享越多無關的知識 → 連動修改越多 → 複雜互動風險越大
- 設計 A → 自由度最大、複雜度也最大;設計 C → 自由度最小、複雜度也最小
「全部限制」並不是答案。一個 Big Ball of Mud 在 Cynefin 上甚至可說屬於 clear 領域——「任何改動都會壞掉」。這不是我們要的簡單性。
我們要的是:約束「共享的知識」,讓需要的互動可行,禁止複雜互動。
重點整理#
評估設計決策時,問自己:
- 這個設計帶來的是線性互動,還是複雜互動?
- 五年後我還能預測它的行為嗎?我會不會被迫靠「實驗」來確認?
- 全域與局部複雜度都在合理範圍嗎?
- 公開介面的自由度是否過高,允許了不該有的輸入或行為?
把複雜度的成因看清楚之後,下一章要面對的是相反的目標——模組化(modularity):軟體設計真正想達成的東西。