本章探討「敏捷設計」的本質,說明為何軟體會腐化(rot),定義七種設計臭味(Design Smells),並透過一個 Copy 程式的演變範例,展示敏捷團隊如何在變更中保持設計品質。
源碼即設計#
Jack Reeves 在 1992 年提出了一個深具影響力的觀點:軟體的源碼本身就是設計(source code IS the design)。工程圖紙之於建築,就如同源碼之於軟體——源碼不是產品,而是產品的設計文件。
設計臭味:腐化軟體的症狀#
當軟體開始腐化,會出現以下七種設計臭味(Design Smells):
- 僵化性(Rigidity):系統難以變更。一個小改動會引發一連串的連鎖修改,開發者無法預估修改的影響範圍
- 脆弱性(Fragility):修改一處,系統在概念上無關的多處同時崩壞。修 bug 反而產生更多 bug
- 不可移動性(Immobility):系統的某些部分可能對其他系統有用,但將其抽離所需的風險與工作量過大,最終選擇重寫而非重用
- 黏滯性(Viscosity):分為設計的黏滯性與環境的黏滯性。當「做對的事」比「做錯的事」更困難時,黏滯性就出現了——例如編譯時間太長,導致開發者傾向做不需要重新編譯的 hack
- 不必要的複雜性(Needless Complexity):設計中包含目前尚無用處的結構。開發者預測未來需求而提前加入的彈性,往往造成系統更難理解與維護
- 不必要的重複(Needless Repetition):同樣的程式碼以稍微不同的形式散佈各處。當需要修改時,必須找到所有複本逐一修改——遺漏任何一處都是 bug
- 晦澀性(Opacity):程式碼難以理解。隨著時間推移,程式碼會愈來愈晦澀,除非開發者有意識地持續保持清晰
mindmap
root((設計臭味))
僵化性 Rigidity
小改動引發連鎖修改
脆弱性 Fragility
修改一處,多處崩壞
不可移動性 Immobility
重用的風險與成本過高
黏滯性 Viscosity
做對的事比做錯的事更難
不必要的複雜性
預測未來需求的過度設計
不必要的重複
相似程式碼散佈各處
晦澀性 Opacity
程式碼難以理解注意: 這些臭味並非來自不良的初始設計,而是隨著需求不斷變更,設計逐步腐化的結果。
為什麼軟體會腐化?#
在非敏捷的環境中,設計之所以腐化,是因為需求以初始設計未曾預見的方式變化。通常我們會責怪需求變更,但身為軟體開發者,我們非常清楚需求一定會變。如果我們的設計因為需求變更就失敗了,那是我們的設計有問題。
敏捷團隊歡迎變更。他們不會在開發初期就試圖預測所有未來的需求,而是保持設計的簡潔與清晰,透過持續的測試、重構與頻繁交付來確保設計能夠優雅地因應變更。
Copy 程式的演變#
以一個簡單的 Copy 程式為例,展示設計如何在需求變更下腐化,以及敏捷方法如何避免這個問題。

Figure 7.1: Copy program structure chart
初始版本#
最初的需求很簡單:從鍵盤讀取字元,寫入印表機。
public void Copy()
{
int c;
while ((c = Keyboard.Read()) != EOF)
Printer.Write(c);
}這段程式碼完美地滿足了當初的需求——乾淨、簡單、優雅。
第一次變更:加入紙帶讀取器#
新需求要求 Copy 也能從紙帶讀取器(paper tape reader)讀取。開發者加了一個布林旗標:
public void Copy(bool ptFlag)
{
int c;
while ((c = (ptFlag ? PaperTape.Read() : Keyboard.Read())) != EOF)
Printer.Write(c);
}這個 ptFlag 讓原本乾淨的程式碼開始出現裂痕。
第二次變更:加入紙帶打孔器#
再一次新需求——Copy 也要能輸出到紙帶打孔器(paper tape punch)。於是又加了一個旗標:
public void Copy(bool ptFlag, bool punchFlag)
{
int c;
while ((c = (ptFlag ? PaperTape.Read() : Keyboard.Read())) != EOF)
{
if (punchFlag)
TapePunch.Write(c);
else
Printer.Write(c);
}
}程式碼持續腐化——僵化、脆弱、不可移動——後續每次新增裝置都必須修改這段程式碼。
敏捷的做法:使用抽象#
敏捷團隊在面對第一次變更時,就會意識到這是一個需要抽象的訊號。使用介面將讀取與寫入行為抽象化:
public interface IReader
{
int Read();
}
public interface IWriter
{
void Write(int c);
}
public void Copy(IReader reader, IWriter writer)
{
int c;
while ((c = reader.Read()) != EOF)
writer.Write(c);
}現在 Copy 程式對擴展是開放的(可加入新的 Reader 與 Writer),對修改是封閉的(不需要改動 Copy 本身)。
重點: 敏捷團隊不會在第一天就過度設計。初始版本沒有介面是正確的,因為當時不需要。但當變更來臨時,團隊利用設計原則(OCP、DIP)與設計模式(Strategy)來重構設計,而非在現有結構上打補丁。
敏捷設計的流程#
敏捷設計不是一個一次性的前期活動,而是一個持續的過程:
- 偵測問題:透過敏捷實踐(測試、短迭代、頻繁交付)盡早發現設計臭味
- 診斷問題:運用設計原則(SRP、OCP、LSP、DIP、ISP)判斷問題根源
- 解決問題:套用適當的設計模式來修正設計
技巧: 敏捷團隊不會預先套用所有原則與模式——他們只在臭味出現時才行動。保持設計盡可能簡單,並在每次迭代中持續改進。
本章小結#
敏捷設計是一個過程,不是事件(a process, not an event)。它是原則、模式與實踐的持續應用,用以保持軟體結構的簡潔與清晰。敏捷開發者不追求「完美的初始設計」,而是在每次迭代中,讓設計對當前的需求保持最佳狀態。