設計是將需求連結到編碼與除錯的活動。無論專案大小,將設計視為明確活動都能帶來好處。
在小型專案中,設計可能只是在編碼前寫下類別介面的虛擬碼;在大型專案中,即使有正式的架構,仍有大量設計工作留給建構階段。

本章探討設計的挑戰、關鍵概念、啟發式方法與實踐技巧。

5.1 設計中的挑戰#

Figure 5-1: The Tacoma Narrows bridge—an example of a wicked problem.

設計本質上是困難的,以下特性說明了為什麼:

特性說明
棘手問題
(Wicked Problem)
你須先「解決」問題一次才能真正定義它,然後再次解決才能得到可行的方案。
塔科馬海峽吊橋(Tacoma Narrows Bridge)就是經典案例——直到橋塌了,工程師才知道需要考慮空氣動力學。
凌亂的過程即使最終結果看起來整潔,過程中充滿錯誤嘗試與死胡同。
在設計階段犯錯的成本遠低於編碼後才發現錯誤。
取捨與優先順序的權衡快速回應 vs. 開發時間、彈性 vs. 簡單性——
設計師必須在競爭的特性之間取得平衡。
涉及限制限制迫使簡化,而簡化往往改善了解決方案。
非確定性的
(Nondeterministic)
同個問題,三個人可能產生三種不同但都合理的設計。
啟發式過程
(Heuristic Process)
沒有保證產生可預測結果的固定步驟,而是依賴經驗法則與試錯。
湧現的
(Emergent)
好設計不會從腦中跳出,而是透過審查、討論、編碼經驗逐步演化。

5.2 關鍵的設計概念#

軟體的首要技術任務:管理複雜度#

Fred Brooks 在〈No Silver Bullets〉中區分了兩類困難:

  • 本質困難(Essential Difficulties):源於軟體須精確對應複雜現實世界需求,
    即使發明了完美程式語言也無法消除
  • 附帶困難(Accidental Difficulties):源於工具與流程的不完善(如笨拙的語法、批次作業),
    大部分已在過去幾十年被解決

軟體的首要技術任務是管理複雜度。
當專案複雜到沒人完全理解修改某處對其他地方的影響時,進度就會停滯。

Dijkstra 指出,沒有頭腦大到能裝下整個現代程式。
因此我們應組織程式,使自己能安全地一次只專注於一個部分——最小化同時需思考的程式量。

對抗複雜度的雙管齊下策略:

  1. 最小化任何人在同時間需處理的本質複雜度
  2. 避免附帶複雜度不必要地增生

設計的理想特性#

特性說明
最小複雜度(Minimal Complexity)避免「聰明」的設計,追求「簡單易懂」的設計
易於維護(Ease of Maintenance)為維護程式設計師而設計,讓系統能自我解釋
鬆散耦合(Loose Coupling)將不同部分之間的連結降到最低
可擴充性(Extensibility)可以增強系統而不破壞底層結構
可重用性(Reusability)系統的組件可以在其他系統中重用
高扇入(High Fan-in)表示系統善用底層的公用類別
低到中等扇出
(Low-to-Medium Fan-out)
一個類別使用超過約七個其他類別,可能過於複雜
可攜性(Portability)容易移植到其他環境
精簡(Leanness)沒有多餘部分;如 Voltaire 所說,
完成不是沒東西可以再加,而是沒東西可以再拿掉
層次化(Stratification)保持分解層次一致,讓你能在任何層級檢視系統
而不必深入其他層級
標準技術
(Standard Techniques)
盡量使用標準化的常見做法,減少陌生感

設計的層次#

設計分為五個層次,每個層次適用不同的技術:

層次範圍說明
1整個軟體系統最高層的組織
2子系統或套件劃分識別主要子系統(資料庫、UI、商業規則等),定義子系統間的通訊規則。
限制子系統間的通訊至關重要,否則分離就失去意義
3套件內類別劃分識別所有類別及其互動方式,定義類別介面
4類別資料與常式劃分定義每個類別的私有常式,進一步細化類別介面
5常式內部設計撰寫虛擬碼、查找演算法、組織程式碼段落

常見子系統包括:商業規則、使用者介面、資料庫存取、系統相依性、第三方元件封裝。
將這些分離可大幅降低系統複雜度。

mindmap
  root(("軟體設計"))
    L1["層次 1:整個軟體系統"]
    L2["層次 2:子系統/套件"]
    L3["層次 3:套件內類別"]
    L4["層次 4:類別內資料與常式"]
    L5["層次 5:常式內部設計"]

5.3 設計構造塊:啟發式方法#

設計是非確定性的,因此啟發式方法(Heuristics)——「有時管用的經驗法則」——
是好設計的核心活動。以下是最重要的設計啟發式方法。

找出現實世界的物件#

物件導向設計的基本步驟:

  1. 識別物件及其屬性(方法與資料)
  2. 決定可對每個物件做什麼
  3. 決定每個物件可對其他物件做什麼
  4. 決定每個物件哪些部分對外可見(公開 vs. 私有)
  5. 定義每個物件公開介面

這些步驟不必按順序執行,且經常需要反覆迭代。

flowchart TD
    step1["1:識別物件及其屬性"]
    step2["2:決定可對每個物件做什麼"]
    step3["3:決定物件之間的互動"]
    step4["4:決定物件可見性(公開 vs. 私有)"]
    step5["5:定義每個物件的公開介面"]

    step1 --> step2 --> step3 --> step4 --> step5
    step5 -- "反覆迭代" --> step1

    style step1 fill:#4dabf7,stroke:#339af0,color:#fff
    style step2 fill:#4dabf7,stroke:#339af0,color:#fff
    style step3 fill:#4dabf7,stroke:#339af0,color:#fff
    style step4 fill:#4dabf7,stroke:#339af0,color:#fff
    style step5 fill:#4dabf7,stroke:#339af0,color:#fff

形成一致的抽象#

Figure 5-7: Abstraction allows you to take a simpler view of a complex concept.

抽象(Abstraction) 是在安全忽略某些細節的情況下處理概念的能力。
基底類別是種抽象,良好類別介面也是種抽象。從複雜度看,抽象的主要好處是讓你忽略不相關的細節。
好的程式設計師在常式介面、類別介面和套件介面層級都建立抽象——分別對應門把、門和房子的層級。

封裝實作細節#

Figure 5-8: Encapsulation says that, not only are you allowed to take a simpler view of a complex concept, you are not allowed to look at any of the details of the complex concept. What you see is what you get—it's all you get!

封裝(Encapsulation) 從抽象的終點開始。
抽象說:「你可以從高層看一個物件。」封裝說:「而且你不被允許從任何其他層級看它。」
封裝透過禁止你查看複雜度來幫助管理複雜度。

適時使用繼承#

繼承(Inheritance) 讓你定義相似物件間的共同點與差異。 它與抽象協同作用——你可以在不同抽象層級上操作。
多型(Polymorphism) 則是語言在執行時期才決定要處理哪種特定類型的能力。
繼承是強大的工具,用得好帶來巨大好處,用得不好則造成嚴重損害。

隱藏秘密(資訊隱藏)#

資訊隱藏(Information Hiding) 是結構化設計與物件導向設計的基礎之一,由 David Parnas 在 1972 年提出。
其核心思想是每個類別(或套件、常式)都有它對外隱藏的「秘密」。

Fred Brooks 在《人月神話》20 周年版中承認:「Parnas 是對的,我對資訊隱藏的批評是錯的。」
Barry Boehm 的研究也表明資訊隱藏是消除重工的強大技術。

秘密的種類:

  • 易變更的區域(如商業規則、硬體相依性)
  • 檔案格式、資料類型的實作方式
  • 需與程式其餘部分隔離以限制錯誤擴散的區域

資訊隱藏的障礙:

  • 過度分散的資訊(如把全域資料的實作細節散布到整個程式)
  • 循環相依性(類別 A 呼叫類別 B,類別 B 又呼叫類別 A)
  • 把類別資料誤認為全域資料而刻意迴避
  • 對效能損失的過早擔憂

養成問「我該隱藏什麼?」的習慣。
你會驚訝有多少困難問題在這問題面前迎刃而解。

識別可能變更的區域#

優秀設計師的共同特質是能夠預見變更。步驟如下:

  1. 識別可能變更的項目
  2. 將易變的元件封裝到各自的類別中
  3. 設計類別間的介面,使其對潛在變更不敏感
常見的易變區域
  • 商業規則:稅制變更、合約條款改變
  • 硬體相依性:螢幕、印表機、通訊裝置的介面
  • 輸入與輸出:檔案格式、使用者介面欄位配置
  • 非標準語言特性:特定實作的延伸功能
  • 困難的設計與建構區域:可能做得不好需要重做的部分
  • 狀態變數:使用列舉型別而非布林值,用存取常式而非直接檢查
  • 資料大小限制:用具名常數(如 MAX_EMPLOYEES)而非寫死的數字

保持耦合鬆散#

耦合(Coupling) 描述類別或常式間關係的緊密程度。目標是建立小型、直接、可見且靈活的關聯。

評估耦合的準則:

  • 大小(Size):連接數越少越好
  • 可見性(Visibility):透過參數列傳遞資料是好的;修改全域資料讓另個模組使用是壞的
  • 彈性(Flexibility):其他模組能多容易地呼叫該模組

耦合的種類(從鬆到緊):

耦合度種類說明
最鬆簡單資料參數耦合所有資料都是基本型別,
透過參數列傳遞——正常且可接受
簡單物件耦合模組實例化另個物件——沒問題
較緊物件參數耦合Object1 要求 Object2 傳遞 Object3
最緊語意耦合(Semantic Coupling)模組依賴對另個模組
內部運作方式的語意知識——最危險

語意耦合是最陰險的耦合形式。例如:模組 A 傳遞控制旗標告訴模組 B 該做什麼;
模組 B 依賴模組 A 已按特定方式修改全域資料。這類耦合會讓系統變脆弱且難維護。

flowchart LR
    A["簡單資料參數耦合\n(基本型別透過參數傳遞)"]
    B["簡單物件耦合\n(實例化另一個物件)"]
    C["物件參數耦合\n(需傳遞第三方物件)"]
    D["語意耦合\n(依賴內部運作知識)"]

    A -- "鬆" --> B -- "較緊" --> C -- "緊" --> D

    style A fill:#69db7c,stroke:#40c057,color:#000
    style B fill:#a9e34b,stroke:#82c91e,color:#000
    style C fill:#ffa94d,stroke:#f76707,color:#000
    style D fill:#ff6b6b,stroke:#fa5252,color:#fff

    A -.- E["✅ 可接受"]
    B -.- E
    C -.- F["⚠️ 需注意"]
    D -.- G["❌ 危險"]

    style E fill:#d3f9d8,stroke:#40c057,color:#000
    style F fill:#fff3bf,stroke:#fcc419,color:#000
    style G fill:#ffe3e3,stroke:#fa5252,color:#000

尋找常見的設計模式#

設計模式(Design Patterns) 透過提供現成的抽象來降低複雜度,並將設計對話提升到更高的層次。

常見設計模式
  • Abstract Factory:建立相關物件的集合,只需指定集合類型
  • Adapter:將類別介面轉換為不同的介面
  • Bridge:讓介面與實作可以獨立變化
  • Composite:物件包含自身類型的其他物件,客戶端只需與頂層物件互動
  • Decorator:動態附加職責而不需為每種組合建立子類別
  • Facade:為不一致的程式碼提供一致的介面
  • Factory Method:實例化衍生類別而不需在其他地方追蹤每個衍生類別
  • Iterator:循序存取集合中的每個元素
  • Observer:讓多個物件保持同步
  • Singleton:提供只有唯一實例的類別的全域存取
  • Strategy:定義一組可動態互換的演算法
  • Template Method:定義演算法結構,將部分實作留給子類別

避免兩個陷阱:(1) 為套用模式而強行扭曲程式碼,(2) 想嘗試某模式而非因它適合而使用它。

其他啟發式方法#

啟發式方法說明
追求強內聚
(Strong Cohesion)
類別中所有常式都應支持同個核心目的
建立階層
(Build Hierarchies)
階層讓你一次只關注一個層級的細節
形式化類別契約
(Formalize Class Contracts)
以前置條件與後置條件思考介面
指派職責
(Assign Responsibilities)
思考每個物件應該負責什麼
為測試而設計
(Design for Test)
分離 UI 與核心邏輯,最小化子系統間的相依性
避免失敗
(Avoid Failure)
考慮系統可能失敗的方式,
而非只複製成功設計的特質
有意識地選擇繫結時間
(Binding Time)
早期繫結較簡單但較不靈活
建立中央控制點
(Central Points of Control)
每個非瑣碎功能都應有「唯一正確的地方」
考慮使用暴力法
(Brute Force)
能運作的暴力解法勝過
不能運作的優雅解法
畫圖
(Draw a Diagram)
圖可以在更高抽象層級呈現問題
保持模組化
(Keep Design Modular)
每個常式或類別都像個黑箱

5.4 設計實踐#

迭代#

設計是迭代的過程——你不會只從 A 點走到 B 點,而是從 A 到 B 再回到 A。
在高層與低層觀點間切換是困難但必要的。第一次設計看來夠好時不要停下來——第二次嘗試幾乎總是更好。

分而治之#

將程式分解到看來夠簡單的層級,然後針對每個部分進行設計。
如果某部分不夠簡單無法直接編碼,就繼續分解。要注意在分解過程中保持對整體全貌的掌握。

由上而下 vs. 由下而上#

  • 由上而下(Top Down):從最高抽象層級開始,逐步增加細節。
    優點是讓你延遲建構的細節,從高層開始分解比從低層開始更容易
  • 由下而上(Bottom Up):從細節開始,向上建構到通用方案。
    優點是傾向於找出可重用的公用功能,早期識別所需的組件

實務上最好結合兩種方法。由上而下分解時遇到困難,就嘗試由下而上;反之亦然。
兩觀點間的拉扯能產生比單靠其中一種更穩固的結構。

flowchart TD
    subgraph top_down["由上而下(Top Down)"]
        direction TB
        TD1["高層抽象"] --> TD2["中層模組"] --> TD3["低層細節"]
    end

    subgraph bottom_up["由下而上(Bottom Up)"]
        direction TB
        BU3["低層元件"] --> BU2["中層組合"] --> BU1["高層方案"]
    end

    TD2 <-- "遇到困難時\n切換方向" --> BU2

    style TD1 fill:#4dabf7,stroke:#339af0,color:#fff
    style TD2 fill:#74c0fc,stroke:#339af0,color:#000
    style TD3 fill:#a5d8ff,stroke:#339af0,color:#000
    style BU3 fill:#a5d8ff,stroke:#339af0,color:#000
    style BU2 fill:#74c0fc,stroke:#339af0,color:#000
    style BU1 fill:#4dabf7,stroke:#339af0,color:#fff

實驗性原型#

原型設計(Prototyping) 是用最少量的可丟棄程式碼來回答特定設計問題。適合用原型來探索的問題包括:

  • 能否使系統達到所需效能?
  • 特定資料庫組織方式能否支援所需查詢?
  • 關鍵演算法的時間/空間效能是否足夠?

原型設計的紀律在於:寫最少量的程式碼來回答問題。
如果開發者無法抵抗將原型細化為產品的衝動,原型設計就不適合你的團隊。

協同設計#

  • 結對程式設計(Pair Programming):兩人合作,所有設計決策都經過即時審查
  • 正式審查(Formal Inspections):三人以上的團隊依循結構化流程審查設計
  • 非正式設計審查:向同事展示設計並徵求回饋

研究顯示,協同開發實踐能找出比測試更高比例的缺陷,且找到的缺陷類型不同。

記錄設計成果#

可用的設計記錄方式包括:將設計決策寫入程式碼註解、使用 Wiki、寫電子郵件摘要、
用數位相機拍攝白板圖、建立 UML 圖、使用 CRC 卡。關鍵是選擇適合專案規模的方式。

設計做到什麼程度才夠?#

沒有標準答案,但以下指引可以參考:

  • 對於小型非正式專案,可能只需在寫程式前花幾分鐘思考設計
  • 對於較大的專案,設計應做到實作看起來顯而易見的程度
  • 設計文件的詳細程度取決於:團隊經驗、團隊成員之間的溝通頻率、專案生命週期的長度
  • 最大的風險不是做了太多或太少設計,而是沒有根據專案特性做出明確的設計決策

5.5 對流行的設計方法的評論#

不要對任何單一方法論過於教條。
設計是啟發式的過程,對任何方法論的死板遵從都會傷害創造力和程式品質。

將設計方法當作工具箱中的工具。有時需要這把鉗子,有時需要那把螺絲起子。
固執只用一種不會讓你成為更好的工匠。多嘗試、多迭代、多協作、追求簡單,你就會對自己的設計滿意。

更多資源#

  • 物件導向通識:Weisfeld《The Object-Oriented Thought Process》、Riel《Object-Oriented Design Heuristics》
  • 設計實踐:Plauger《Programming on Purpose》、Meyer《Object-Oriented Software Construction》
  • 設計模式:Gamma 等《Design Patterns》(Gang of Four)、Shalloway & Trott《Design Patterns Explained》
  • 設計理論:Parnas & Clements〈A Rational Design Process: How and Why to Fake It〉、Parnas〈On the Criteria to Be Used in Decomposing Systems into Modules〉
  • 通用問題解決:Polya《How to Solve It》、Adams《Conceptual Blockbusting》

要點#

  • 軟體的首要技術任務是管理複雜度,這得力於以簡單性為核心的設計。
  • 簡單性透過兩方式達成:最小化任何人同時需處理的本質複雜度避免附帶複雜度增生
  • 設計是啟發式的。對任何單一方法論的教條式遵從都會傷害創造力與程式品質。
  • 好的設計是迭代的;你嘗試的設計可能性越多,最終設計就越好。
  • 資訊隱藏是特別有價值的概念。問「我應該隱藏什麼?」能解決許多困難的設計問題。
  • 大量有用的設計資訊存在於本書之外。本章呈現的觀點只是冰山一角。