設計的聖杯:簡單#
設計是軟體工藝的聖杯與終極目標——我們追求的是一種完美到可以毫不費力地添加功能、經歷數月甚至數年的維護後仍然保持靈活的設計。
然而本章的重點不在設計原則、設計模式或架構——這些值得深入研究,但設計的關鍵本質可以用一個詞來概括:簡單(Simplicity)。
正如 Chet Hendrickson 所說:
Uncle Bob wrote thousands of pages on clean code. Kent Beck wrote four lines.
本章聚焦的就是 Kent Beck 的四條簡單設計規則。
簡單的定義#
- 簡單不等於容易——簡單意味著未糾纏(untangled),而解開糾纏是困難的
- 軟體系統中最昂貴、最嚴重的糾纏,是高階策略(high-level policies)與低階細節(low-level details)的交纏
- SQL 與 HTML 混在一起
- 框架與核心價值混在一起
- 報告格式與計算業務規則混在一起
- 這些糾纏容易寫出來,卻使得新增功能、修復 bug、改善設計都變得困難
簡單設計是指高階策略對低階細節一無所知的設計。高階策略被隔離、保護,使得低階細節的變更對高階策略毫無影響。
抽象與多型#
實現隔離的主要手段是抽象(Abstraction)——放大本質(高階策略),消除無關(低階細節)。
物理實現手段則是多型(Polymorphism):
- 高階策略透過多型介面來管理低階細節
- 低階細節作為多型介面的實作
- 所有原始碼依賴方向從低階細節指向高階策略
- 低階細節可以被變更而不影響高階策略

Figure 6.1: Polymorphism
最佳設計應具有最少的抽象,同時仍能有效隔離高階策略與低階細節。
YAGNI#
從過度設計到 YAGNI#
1980-1990 年代的策略恰好相反——程式設計師痴迷於為預期的未來變更預先放置掛鉤(hooks)。這是因為當時軟體難以修改:
- 小型系統可能需要一小時以上來建置
- 測試全是手動的,因此極度不充分
- 程式設計師越來越害怕做出改變,導致過度設計
1990 年代末 Extreme Programming 和 Agile 的出現改變了一切——機器變得強大到建置時間可以縮短到秒級,自動化測試也變得可負擔。
YAGNI 的真正含義#
1999 年,Kent Beck 提出了 YAGNI 的真正問題:
What if you aren’t gonna need it?(如果你不需要它呢?)
- 每次想放入掛鉤時,問自己:如果不放會怎樣?
- 如果不放的成本可以接受,就可能不應該放
- 如果長年維護掛鉤的成本高,但最終需要它的機率低,也可能不應該放
- 掛鉤的問題在於我們很少猜對——我們不擅長預測客戶實際會做什麼
YAGNI 不代表永遠不要預先思考。有時放入特定掛鉤確實是好主意。但取捨已經發生了劇烈變化——現在通常更好的做法是留下大多數掛鉤不放,在需求變化時重構設計。
作者指出一個巨大的諷刺:Moore’s Law 的指數增長驅動我們建造越來越複雜的軟體系統,同時也使我們能夠大幅簡化這些系統的設計。
四條簡單設計規則#
規則的演進#
這四條規則隨時間不斷演進:
| # | 1999 年(Beck 原版) | 2015 年(Fowler 版) |
|---|---|---|
| 1 | 系統(程式碼和測試)必須傳達你想傳達的一切 | Passes the tests |
| 2 | 系統不得包含重複程式碼 | Reveals intention |
| 3 | 系統應有最少的 class | No duplication |
| 4 | 系統應有最少的 method | Fewest elements |
注意規則的演進方向:第一條規則從「溝通」分裂為測試與表達兩個面向,最後兩條合併為一條。而且隨著時間推移,測試的重要性從溝通提升到了覆蓋率。
規則一:Covered by Tests(被測試覆蓋)#
覆蓋率的歷史#
程式碼覆蓋率的概念非常古老——最早可追溯至 1963 年,距第一台電子計算機上執行第一個程式僅 17 年。Miller 和 Maloney 在論文中就已指出:
要對任何程式有信心,不能只知道它大多數時候運作正常。真正的問題是,每一次都能成功完成功能規格嗎?程式的每個部分都必須在測試中被使用。
覆蓋率的目標#
什麼是好的覆蓋率數字?作者的答案:100%。
- 如果你滿足於 80% 的覆蓋率,代表你不知道 20% 的程式碼是否正常運作
- 100% 行覆蓋率和 100% 分支覆蓋率是目標
- 這可能是一個漸近目標(asymptotic goal)——也許永遠無法達到,但這不是不嘗試的藉口
- 作者親身參與過成長到數萬行程式碼的專案,始終將覆蓋率維持在接近 100% 的水準
覆蓋率與設計的關係#
可測試的程式碼就是解耦的程式碼。
- 要達到高覆蓋率,程式碼的每個部分都必須能被測試存取
- 這意味著每個部分必須與其他程式碼充分解耦,才能被隔離並從測試中呼叫
- 因此,測試不僅是行為的測試,也是解耦的測試
- 撰寫隔離測試的行為本身就是一種設計行為
測試還有另一層好處:一套可信賴的測試套件大幅降低了對變更的恐懼。當需求改變時,你可以無所畏懼地調整設計。這就是為什麼這是四條規則中最重要的一條——沒有測試套件,其他三條規則變得不切實際,因為那些規則涉及重構,而重構離不開測試。
flowchart LR
A["高覆蓋率"] --> B["程式碼可被測試存取"]
B --> C["充分解耦"]
C --> D["撰寫測試 = 設計行為"]
D --> E["降低變更恐懼"]
E --> F["安全重構"]
F --> G["持續改善"]規則二:Maximize Expression(最大化表達)#
程式碼的表達力#
在早期程式設計年代,「code」這個詞本身就暗示意圖是被掩蓋的。早期程式碼完全無法顯示意圖,因此需要大量註解。

Figure 6.2: An example of an early program
但現代語言具有強大的表達力。透過適當的紀律,我們可以產生讀起來像「well-written prose that never obscures the designer’s intent」的程式碼。作者以 Video Store 範例中的 RentalCalculator class 為例——變數、函式和型別的名稱具有深層描述性,演算法的結構清晰可見。
底層抽象(The Underlying Abstraction)#
表達力不只是漂亮的命名,還涉及層次的分離與底層抽象的揭示。
作者以薪資系統為例——按小時計薪的員工、領佣金的員工、領固定薪資的員工各有不同的規則。容易想像一組帶有複雜 switch 或 if/else 鏈的函式來處理這些需求,但這樣會掩蓋底層抽象。真正的底層抽象是:
public List<Paycheck> run(Database db) {
Calendar now = SystemTime.getCurrentDate();
List<Paycheck> paychecks = new ArrayList<>();
for (Employee e : db.getAllEmployees()) {
if (e.isPayDay(now))
paychecks.add(e.calculatePay());
}
return paychecks;
}將高階策略與低階細節分離,是讓設計簡單且富有表達力的最根本做法。
測試:問題的另一半#
回顧 Beck 最初的第一條規則:「系統(程式碼和測試)必須傳達你想傳達的一切。」
- 無論生產程式碼多有表達力,它都無法傳達使用情境——這是測試的工作
- 每個測試(尤其是隔離且解耦的測試)都是程式碼使用方式的示範
- 寫得好的測試就是被測程式碼的使用案例範本
- 程式碼和測試合在一起,表達了系統每個元素做什麼以及如何使用
規則三:Minimize Duplication(最小化重複)#
重複的起源與危害#
- 早期程式設計沒有原始碼編輯器,用 #2 鉛筆寫在預印表格上,最好的編輯工具是橡皮擦——所以不會重複程式碼
- 隨著原始碼編輯器和複製/貼上的出現,越來越多系統展現出大量程式碼重複
- 重複的危害:兩段或多段相似的程式碼往往需要同時修改,找到它們很難,正確修改更難(因為它們存在於不同情境中)
- 重複導致脆弱性(fragility)
消除重複的手段#
- 一般做法:將相似程式碼抽象為新函式,提供適當參數以溝通情境差異
- 當重複出現在遍歷複雜資料結構的程式碼中時,可使用 Lambda、Command 物件、Strategy 模式或 Template Method 模式來封裝遍歷邏輯
意外重複(Accidental Duplication)#
並非所有重複都應被消除。 有時兩段程式碼可能非常相似甚至完全相同,但會因不同原因而改變——這稱為意外重複。意外重複應被保留,隨著需求變化,它們會自然地分別演化,重複也會自然消失。
判斷真正重複與意外重複的方法:
- 意外重複的意圖是發散的
- 真正重複的意圖是收斂的
管理重複並不簡單——識別哪些是真正重複、哪些是意外重複取決於程式碼的表達力,而封裝和隔離真正重複則需要大量的重構,重構又需要良好的測試套件。
因此,消除重複在四條規則中排名第三——測試和表達力排在它前面。
規則四:Minimize Size(最小化大小)#
簡單設計由簡單元素組成,簡單元素是小的。最後一條規則:
- 在所有測試通過後
- 在程式碼盡可能有表達力後
- 在重複最小化後
- 在不違反前三條規則的前提下,努力縮小每個函式中的程式碼量
做法主要是擷取更多函式——持續擷取直到無法再擷取,最終得到漂亮的小函式、有意義的長名稱,使函式既小又有表達力。
flowchart TD
R1["規則 1:被測試覆蓋"] --> R2["規則 2:富有表達力"]
R2 --> R3["規則 3:消除重複"]
R3 --> R4["規則 4:最小化程式碼量"]
R4 -.->|"依賴"| R3
R3 -.->|"依賴"| R2
R2 -.->|"依賴"| R1四條規則的終極承諾#
Kent Beck 曾對作者說:如果你盡可能認真地遵循這四件事——覆蓋率、表達力、單一化、精簡化——那麼所有其他設計原則都會被滿足。
作者坦言不確定這是否完全正確——不確定一個完美覆蓋、表達、去重、精簡的程式是否必然符合 Open-Closed Principle 或 Single Responsibility Principle。但他非常確定的是:了解和研究良好的設計原則與架構(例如 SOLID 原則)會使創建分區良好的簡單設計容易得多。