
software-design-philosophy
熱門透過深層模組、資訊隱藏與策略性程式設計來管理軟體複雜度。當使用者提及「模組設計」、「API 太複雜」、「淺層類別」、「複雜度預算」、「策略 vs 戰術」、「深層模組」、「資訊洩漏」、「轉送方法」、「這段程式碼過度設計」或「簡化這個設計」時使用。也適用於審查介面簡潔度、評估抽象是否值得、決定是否該寫註解,或選擇通用 vs 專用方法。涵蓋深層與淺層模組、複雜度紅旗,以及作為設計文件的註解。關於程式碼品質,請參閱 clean-code。關於架構邊界,請參閱 clean-architecture。
透過深層模組、資訊隱藏與策略性程式設計來管理軟體複雜度。當使用者提及「模組設計」、「API 太複雜」、「淺層類別」、「複雜度預算」、「策略 vs 戰術」、「深層模組」、「資訊洩漏」、「轉送方法」、「這段程式碼過度設計」或「簡化這個設計」時使用。也適用於審查介面簡潔度、評估抽象是否值得、決定是否該寫註解,或選擇通用 vs 專用方法。涵蓋深層與淺層模組、複雜度紅旗,以及作為設計文件的註解。關於程式碼品質,請參閱 clean-code。關於架構邊界,請參閱 clean-architecture。
軟體設計哲學框架
一個實用框架,用於管理軟體工程的基本挑戰:複雜度。在設計模組、審查 API、重構程式碼或提供架構決策建議時,應用這些原則。
核心原則
撰寫軟體的最大限制,是我們理解自己所建立系統的能力。 複雜度是敵人:它讓系統難以理解、難以修改,並成為錯誤的來源。評估每個設計決策時,問自己「這會增加還是減少系統的整體複雜度?」——目標不是零複雜度,而是最小化不必要的複雜度,並將必要的複雜度集中到可管理的地方。
評分
目標:10/10。 在審查或建立設計時,根據滿足多少個快速診斷列來評分(每列約 1.25 分),然後對照區間進行合理性檢查:
- 9-10 — 深層模組,介面遠比實作簡單;沒有資訊洩漏(實作可以改變而不影響呼叫者);介面註解捕捉設計意圖;設計改進是常態。八項診斷全部通過。
- 6-8 — 大致深層,但有一兩個洩漏、淺層類別或未記錄的抽象。5-6 項診斷通過。
- 3-5 — 類別炎或時間分解、反覆洩漏、僅重述程式碼的註解。2-4 項診斷通過。
- ≤2 — 戰術龍捲風程式碼:淺層模組、普遍洩漏、未記錄設計意圖。0-1 項診斷通過。
務必說明當前分數、未通過的診斷列,以及每項需要哪些具體改變才能達到 10/10。
軟體設計框架
管理複雜度並產出易於理解和修改的系統的六項原則:
1. 複雜度及其成因
核心概念: 複雜度是指系統結構中任何使其難以理解和修改的部分。它表現為三種症狀——變更放大、認知負荷和未知的未知——並有兩個成因:依賴關係和模糊性。
關鍵見解:
- 變更放大:一個簡單的變更需要修改多處
- 認知負荷:開發者必須記住太多資訊才能進行變更
- 未知的未知:不明顯需要改變什麼或哪些資訊相關——這是最糟的症狀
- 複雜度是漸進的——它由數百個小決策累積而成(「千刀萬剮」),因此每個決策都很重要
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 變更放大 | 集中共享知識 | 提取顏色常數,而不是在 20 個檔案中硬編碼 #ff0000 |
| 認知負荷 | 減少開發者需知道的資訊 | open(path) 而非要求緩衝區大小、編碼、鎖定模式 |
| 未知的未知 | 讓依賴關係明確 | 型別系統和介面揭示變更影響的範圍 |
| 模糊性 | 精確命名 | numBytesReceived 而非 n;retryDelayMs 而非 delay |
請參閱 references/complexity-symptoms.md 當你需要指出程式碼庫有哪種症狀後再修復——每種症狀的辨識測試、依賴關係分類(語法/語意/時間/隱藏)、C = Σ(cp·tp) 成本公式,以及一個 10 列的紅旗表格。
2. 深層 vs 淺層模組
核心概念: 最好的模組是深層的:強大的功能背後是簡單的介面。淺層模組的介面相對於其提供的功能來說過於複雜——它們增加複雜度而非隱藏它。
為什麼有效: 介面是模組對系統其他部分施加的成本;實作則是效益。因此,一個比你自己重新實作還難學的方法就是淨負值——深度,而非程式碼行數,決定了一個模組是否值得存在。
關鍵見解:
- 深度 = 提供的功能 / 施加的介面複雜度(Unix 檔案 I/O 是深層的;薄薄的 Java I/O 包裝是淺層的)
- 「類別炎」:建立過多小型淺層類別的疾病——每個介面都增加認知負荷
- 小方法本身並非好事;深度比大小更重要
- 最好的抽象將顯著的複雜度隱藏在幾個簡單概念之後
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 深層模組 | 將複雜度隱藏在簡單 API 後 | file.read(path) 隱藏磁碟區塊、快取、緩衝、編碼 |
| 類別炎治療 | 合併相關的淺層類別 | RequestParser + RequestValidator + RequestProcessor → 一個 RequestHandler |
| 介面簡潔 | 更少的參數、更少的方法 | config.get(key) 搭配合理的預設值,而非 15 個建構子參數 |
請參閱 references/deep-modules.md 當判斷一個抽象是否值得——深度比的前後程式碼、類別炎治療的詳細步驟,以及案例研究(Unix I/O、GC、TCP/IP)。
3. 資訊隱藏與洩漏
核心概念: 每個模組應封裝其他模組不需要的知識。資訊洩漏——一個設計決策反映在多個模組中——是軟體設計中最重要的紅旗之一。
為什麼有效: 存在於一個模組中的決策可以在那裡改變,其他地方不變;同樣的決策洩漏到 N 個模組,會將一次編輯變成 N 次編輯,而且編譯器不會提醒你。隱藏正是將變更放大轉回局部變更的方法。
關鍵見解:
- 時間分解導致洩漏:按何時發生來拆分程式碼會強制跨模組共享知識——應按知識來組織
- 透過資料格式、協定或共享假設的後門洩漏是最微妙的形式
- 裝飾者經常洩漏——它們暴露被裝飾的介面
- 如果兩個模組共享知識,要麼合併它們,要麼建立一個封裝該知識的新模組
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 格式洩漏 | 集中序列化 | 一個模組擁有 JSON 編碼/解碼,而非到處 json.dumps |
| 時間分解 | 按知識而非時間組織 | 將「讀取設定」和「套用設定」合併為一個設定模組 |
| 協定洩漏 | 抽象傳輸細節 | MessageBus.send(event) 隱藏 HTTP vs. gRPC vs. 佇列 |
請參閱 references/information-hiding.md 當一個變更迫使你同步編輯兩個模組時——四種洩漏形式(介面、後門、時間、裝飾者)附程式碼、五種減少策略、HTTP 處理案例研究,以及一個偵測表格。
4. 通用 vs 專用模組
核心概念: 設計「某種程度上通用」的模組:介面足夠通用以支援多種用途,實作則處理當前需求。問:「能涵蓋我所有當前需求的最簡單介面是什麼?」
為什麼有效: 違反直覺的是,通用介面通常更簡單——特殊情況方法會隨著需求增長而倍增,而一個通用方法可以吸收它們。陷阱是另一方向:當前需求不需要的通用性是投機複雜度,為可能永遠不會到來的使用案例付出代價。
關鍵見解:
- 「某種程度上通用」是太特定和太通用之間的最佳點
- 將複雜度向下推:底層模組應處理困難情況,讓上層保持簡單
- 設定參數通常代表未能做出決定——每個參數都是推給呼叫者的複雜度
- 有疑問時,先實作更簡單、更通用的方法
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| API 通用性 | 為概念而非單一使用案例設計 | text.insert(position, string) 而非 text.addBulletPoint() |
| 減少設定 | 自動決定行為 | 自動偵測檔案編碼,而非 encoding 參數 |
| 避免過度專用化 | 一個通用方法勝過多個特定方法 | store(key, value, options) 而非 storeUser()、storeProduct()、storeOrder() |
請參閱 references/general-vs-special.md 當選擇介面應多通用時——「所有當前需求的最簡單介面」測試、設定參數反模式,以及將複雜度向下推的詳細步驟。
5. 作為設計文件的註解
核心概念: 註解應描述程式碼中不明顯的內容:設計意圖、抽象理由、不變量和假設。「好的程式碼會自我文件化」是一個迷思,僅對低層實作細節成立。
為什麼有效: 程式碼只能記錄做了什麼——永遠無法說明為什麼選擇這種方法而非其他,或它默默假設了什麼。這個理由是系統中最易流失的資訊:它只存在於作者腦中,一旦作者離開就消失,因此註解是捕捉它的唯一機會。
關鍵見解:
- 四種類型:介面註解(最重要——它們定義抽象)、資料結構成員註解、實作註解、跨模組註解
- 先寫註解(註解驅動設計)以在寫程式碼前釐清思路
- 不要重複程式碼已說明的內容;將註解放在它們描述的程式碼旁邊,並一起更新
- 如果註解很難寫,設計可能太複雜
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 介面註解 | 描述抽象,而非實作 | 「回傳最接近位置的 widget,若無則回傳 null」 |
| 資料結構註解 | 解釋不變量 | 「列表按優先級降序排序;平手時按插入順序」 |
| 實作註解 | 解釋為什麼,而非做什麼 | 「// 二元搜尋:列表始終排序,可容納 10 萬+ 項目」 |
| 跨模組註解 | 連結相關決策 | 「// 此逾時必須與 RetryPolicy.java 中的重試間隔一致」 |
請參閱 references/comments-as-design.md 當撰寫或審查註解且不確定應包含什麼時——四種註解類型附範例、註解驅動設計程序,以及對自我文件化程式碼迷思的反駁。
6. 策略 vs 戰術程式設計
核心概念: 戰術程式設計快速讓功能運作,並隨著每個捷徑累積複雜度。策略程式設計投入 10-20% 的額外努力在良好設計上,將每個變更視為改善結構的機會。
為什麼有效: 戰術速度是借來的:每個捷徑讓未來變更更困難,而策略投資會複利——策略性設計的系統在幾個月內就會變得更易於開發。
關鍵見解:
- 戰術龍捲風:快速交付但留下破壞的開發者——短期受讚揚,長期具破壞性
- 你的主要工作是做出一個恰好能運作的偉大設計,而不是一個恰好有設計的運作程式碼
- 新創公司最需要策略程式設計——早期捷徑會隨著團隊成長複合成癱瘓性債務
- 每個變更都是投資機會:讓程式碼變得更好一點;重構是每個功能的一部分,而非特殊事件
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 戰術陷阱 | 抵制快速骯髒的修復 | 不要為「就這一個特殊情況」添加布林參數 |
| 策略投資 | 在功能開發期間改善結構 | 在新增功能時重構尷尬的模組介面 |
| 設計審查 | 評估結構,而非僅正確性 | 問「這會讓系統更簡單嗎?」而不只是「它能運作嗎?」 |
請參閱 references/strategic-programming.md 當決定一個變更應投入多少設計努力,或為其辯護時——10-20% 投資數學、戰術龍捲風模式,以及為什麼新創公司最需要策略程式設計。
常見錯誤
| 錯誤 | 為什麼失敗 | 修正 |
|---|---|---|
| 建立過多小型類別 | 類別炎增加介面而無深度;每個邊界都是認知負擔 | 將相關的淺層類別合併為更深層的模組 |
| 按時間順序拆分模組 | 「讀取、處理、寫入」強制跨模組共享知識 | 將共享知識的程式碼分組到一個模組 |
| 在介面中暴露實作 | 呼叫者依賴內部實作;變更會傳播 | 圍繞抽象設計介面;隱藏格式和協定 |
| 將註解視為可選 | 設計意圖和假設遺失;新人猜錯 | 先寫介面註解;與程式碼一起維護 |
| 萬物皆設定參數 | 參數推給呼叫者是你拒絕做出的決定(見 §4) | 自動決定行為;提供合理的預設值 |
| 快速骯髒的戰術修復 | 捷徑累積直到系統無法運作 | 投入 10-20% 額外努力;將每個變更視為設計機會 |
| 轉送方法 | 僅轉送引數的方法增加介面但無功能 | 將轉送合併到呼叫者或被呼叫者 |
| 為特定使用案例設計 | 專用介面累積特殊情況 | 問:涵蓋所有當前需求的最簡單介面? |
快速診斷
| 問題 | 如果否 | 行動 |
|---|---|---|
| 你能用一句話描述每個模組嗎? | 模組做太多或缺乏目的 | 拆分為連貫、可描述的職責 |
| 介面是否比實作簡單? | 模組淺層——複雜度向外洩漏 | 隱藏更多;將淺層類別合併為更深層的 |
| 你能在不影響呼叫者的情況下改變實作嗎? | 資訊正在跨邊界洩漏 | 將洩漏的知識封裝在一個模組中 |
| 介面註解是否描述抽象? | 設計意圖遺失;模組將被誤用 | 記錄模組承諾什麼,而非如何運作 |
| 設計討論是否納入程式碼審查? | 審查捕捉錯誤但不捕捉複雜度增長 | 將「這是否減少複雜度?」加入審查標準 |
| 每個模組是否隱藏一個重要的設計決策? | 模組圍繞程式碼而非資訊組織 | 重新組織,使每個模組擁有特定知識 |
| 新人能否在不閱讀實作的情況下理解模組邊界? | 抽象未記錄或洩漏 | 改善介面註解;簡化介面 |
| 你是否花費 10-20% 的時間在設計改善上? | 債務隨著每個功能累積 | 在每個 PR 中包含設計改善 |
延伸閱讀
如需包含詳細範例的完整方法論:
- 《軟體設計哲學》 作者 John Ousterhout(第二版)
關於作者
John Ousterhout 是史丹佛大學電腦科學系的 Bosack Lerner 教授,也是 Tcl 腳本語言和 Tk 工具包的創作者。他從史丹佛 CS 190 課程中發展出《軟體設計哲學》,將數十年的系統建構經驗提煉為適用於各種語言和規模的原則。





