本章探討「敏捷設計」的本質,說明為何軟體會腐化(rot),定義七種設計臭味(Design Smells),並透過一個 Copy 程式的演變範例,展示敏捷團隊如何在變更中保持設計品質。

源碼即設計#

Jack Reeves 在 1992 年提出了一個深具影響力的觀點:軟體的源碼本身就是設計(source code IS the design)。工程圖紙之於建築,就如同源碼之於軟體——源碼不是產品,而是產品的設計文件。

設計臭味:腐化軟體的症狀#

當軟體開始腐化,會出現以下七種設計臭味(Design Smells):

  1. 僵化性(Rigidity):系統難以變更。一個小改動會引發一連串的連鎖修改,開發者無法預估修改的影響範圍
  2. 脆弱性(Fragility):修改一處,系統在概念上無關的多處同時崩壞。修 bug 反而產生更多 bug
  3. 不可移動性(Immobility):系統的某些部分可能對其他系統有用,但將其抽離所需的風險與工作量過大,最終選擇重寫而非重用
  4. 黏滯性(Viscosity):分為設計的黏滯性環境的黏滯性。當「做對的事」比「做錯的事」更困難時,黏滯性就出現了——例如編譯時間太長,導致開發者傾向做不需要重新編譯的 hack
  5. 不必要的複雜性(Needless Complexity):設計中包含目前尚無用處的結構。開發者預測未來需求而提前加入的彈性,往往造成系統更難理解與維護
  6. 不必要的重複(Needless Repetition):同樣的程式碼以稍微不同的形式散佈各處。當需要修改時,必須找到所有複本逐一修改——遺漏任何一處都是 bug
  7. 晦澀性(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)來重構設計,而非在現有結構上打補丁。

敏捷設計的流程#

敏捷設計不是一個一次性的前期活動,而是一個持續的過程

  1. 偵測問題:透過敏捷實踐(測試、短迭代、頻繁交付)盡早發現設計臭味
  2. 診斷問題:運用設計原則(SRP、OCP、LSP、DIP、ISP)判斷問題根源
  3. 解決問題:套用適當的設計模式來修正設計

技巧: 敏捷團隊不會預先套用所有原則與模式——他們只在臭味出現時才行動。保持設計盡可能簡單,並在每次迭代中持續改進。

本章小結#

敏捷設計是一個過程,不是事件(a process, not an event)。它是原則、模式與實踐的持續應用,用以保持軟體結構的簡潔與清晰。敏捷開發者不追求「完美的初始設計」,而是在每次迭代中,讓設計對當前的需求保持最佳狀態。