設計(Design)是將需求連結到編碼與除錯的活動。無論專案大小,將設計視為明確的活動都能帶來好處。在小型專案中,設計可能只是在編碼前寫下類別介面的虛擬碼;在大型專案中,即使有正式的架構,仍有大量設計工作留給建構階段。本章探討設計的挑戰、關鍵概念、啟發式方法與實踐技巧。

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》

要點#

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