本書大部分篇幅聚焦於建構的具體細節:類別、變數命名、迴圈、排版、系統整合等。本章將這些具體議題背後的抽象主題串連起來——複雜度、抽象、流程、可讀性、迭代等——這些主題正是「隨意拼湊」與「軟體工藝」之間的關鍵差異。
34.1 征服複雜性#
管理複雜度(Managing Complexity) 是軟體開發的首要技術命令。沒有人的大腦能同時橫跨九個數量級的細節,因此整本書介紹的眾多技巧,其核心動機都是降低複雜度:
降低複雜度的常見手段
- 在架構層級將系統分割為子系統,縮小同時需要關注的範圍
- 精心定義類別介面,讓使用者可以忽略內部實作
- 維護類別介面所代表的抽象,避免大腦記憶任意細節
- 避免全域資料——它會大幅增加任何時刻需要同時掌握的程式碼量
- 避免深層繼承階層、深層巢狀迴圈與條件式
- 避免 goto,因為非線性流程對多數人而言難以追蹤
- 系統化的錯誤處理策略,而非任意增生不同手法
- 保持常式短小、變數命名清晰自明
- 最小化常式參數數量,使用慣例來消除各段程式碼之間的任意差異
抽象(Abstraction) 是管理複雜度最有力的工具。從機器語言到高階語言的跳躍是電腦科學史上最大的單次進步(Brooks 1995)。常式、類別、套件都是進一步提升抽象層次的里程碑。用功能性名稱命名變數(描述「什麼」而非「如何」)、使用具名常數取代字面值、以布林函式封裝複雜判斷——每一個小步驟都在降低複雜度。
軟體設計與建構的首要目標是征服複雜度。降低複雜度可以說是成為高效程式設計師最重要的關鍵。
34.2 挑選開發程序#
流程(Process) 對軟體品質的影響超乎想像。小型專案中個人才能是最大因素,但多人專案中,組織特性比個人技能更具影響力——團隊合作方式決定了能力是「相加」還是「相減」。
流程為何重要的幾個面向:
- 需求穩定性:不先穩定需求就開始設計與編碼,後續變更會不斷侵蝕系統品質。若需要彈性,可採用增量式開發(Incremental Development)分批交付。需求錯誤的修正成本遠高於建構錯誤。
- 設計先於編碼:先打穩地基再施工。匆忙進入編碼階段後,人們對既有設計產生情感投入,要推翻不良基礎就更困難。
- 品質必須從第一步就內建:「先亂寫再測試除錯」的想法完全錯誤。測試只能告訴你軟體在哪些方面有缺陷,無法讓程式更易用、更快、更小或更可讀。
- 避免過早最佳化(Premature Optimization):有效的流程是先粗調、後微調。把時間花在打磨不需要打磨的程式碼上,或捨不得丟棄已打磨過的壞程式碼,都是浪費。
持續關注自己如何創造軟體。好的流程能最大化運用你的大腦,壞的流程則浪費腦力。
34.3 首先為人寫程式,其次才是為機器#
可讀性(Readability) 是貫穿全書的另一大主題。電腦不在乎你的程式碼是否可讀——它更擅長讀二進位指令。你寫可讀的程式碼是為了幫助其他人(包括未來的自己)。
可讀性對程式的正面影響:可理解性、可審查性、錯誤率、除錯、可修改性、開發時間、外部品質。
撰寫可讀程式碼長期來看不會花更多時間。偏好「寫入時方便」而犧牲「讀取時方便」是一種假性節省——你只寫一次,卻會讀很多次。研究顯示,平均一支程式在改寫前會經歷 10 代維護程式設計師,維護者有 50%–60% 的時間花在試圖理解程式碼上(Thomas 1984; Parikh and Zvegintzov 1983)。
「反正只有我會看這段程式碼」——請確認你沒有把因果關係搞反。也許正因為程式碼難讀,所以沒人想看。養成寫可讀程式碼的習慣,因為習慣無法隨意開關。
34.4 用程式去創造適合的語言環境,而不是遷就它#
最好的程式設計師會先思考要做什麼,再評估如何用手邊的工具達成目標——而不是把思維侷限在語言自動支援的概念裡。
- 語言不支援斷言?自己寫
assert()常式。 - 語言不支援列舉型別或具名常數?用全域變數搭配清晰命名慣例來模擬。
- 不要因為語言支援全域資料或 goto 就使用它們;用程式設計慣例來彌補語言弱點。
核心概念:Program into your language, not in it. 先確立技術目標,再決定如何在語言中實現,而非順著語言的最明顯路徑走。
34.5 藉助規範集中注意力#
慣例(Conventions) 是管理複雜度的知識工具之一。程式設計中許多細節本質上是任意的(縮排幾格、註解格式、類別中常式的排列順序),具體怎麼選不如每次一致地選重要。
慣例的效益:
| 效益 | 說明 |
|---|---|
| 簡潔傳遞資訊 | 命名慣例中的一個字元就能區分區域、類別與全域變數;大小寫能區分型別、常數與變數 |
| 防護已知風險 | 例如禁止全域變數、要求複雜運算式加括號、刪除指標後立即設為 null |
| 增加可預測性 | 統一的記憶體請求、錯誤處理、I/O 與類別介面處理方式,讓讀者能合理預期 |
| 彌補語言弱點 | 在不支援具名常數的語言中,用慣例區分可讀寫變數與唯讀常數 |
大型專案有時慣例過多,記住它們本身就成了全職工作;小型專案則往往慣例不足,未能充分受益。關鍵在於在需要結構的地方適度運用。
34.6 根據問題領域的術語做程式設計#
另一個應對複雜度的方法是盡可能在最高抽象層次工作——用問題領域(Problem Domain)的詞彙而非電腦科學的解法來思考。頂層程式碼不應充斥檔案操作、堆疊、佇列和名為 i、j、k 的變數,而應包含描述性的類別名稱與常式呼叫。
程式的抽象層次#
| 層級 | 說明 |
|---|---|
| Level 4 — 高階問題領域術語 | 非技術人員也能大致讀懂的程式碼;依賴 Level 3 建構的工具而非語言特性 |
| Level 3 — 低階問題領域術語 | 業務物件層或服務層;提供解決問題所需的詞彙與建構區塊 |
| Level 2 — 低階實作結構 | 堆疊、佇列、鏈結串列、排序演算法等資料結構與演算法 |
| Level 1 — 程式語言結構與工具 | 語言原始型別、控制結構、標準函式庫 |
| Level 0 — 作業系統操作與機器指令 | 高階語言會自動處理此層 |
flowchart BT
L0["Level 0<br/>作業系統操作與機器指令"] --> L1["Level 1<br/>程式語言結構與工具"]
L1 --> L2["Level 2<br/>低階實作結構<br/>(堆疊、佇列、排序等)"]
L2 --> L3["Level 3<br/>低階問題領域術語<br/>(業務物件、服務層)"]
L3 --> L4["Level 4<br/>高階問題領域術語<br/>(非技術人員可讀懂)"]
L4 -. "應盡可能在高層工作" .-> goal["目標:以問題領域<br/>的詞彙來程式設計"]
style L0 fill:#f8d7da,stroke:#dc3545
style L1 fill:#fff3cd,stroke:#ffc107
style L2 fill:#fff3cd,stroke:#ffc107
style L3 fill:#d4edda,stroke:#28a745
style L4 fill:#c3e6cb,stroke:#28a745
style goal fill:#bee5eb,stroke:#17a2b8,stroke-dasharray: 5 5在問題領域中工作的低階技巧#
- 使用類別實作問題領域中有意義的結構
- 隱藏低階資料型別的實作細節
- 使用具名常數記錄字串與數值字面值的意義
- 指派中間變數來記錄中間計算結果
- 使用布林函式釐清複雜的布林測試
34.7 注意落石#
程式設計既非純藝術也非純科學,而是介於兩者之間的工藝(Craft)。良好的判斷力包括對各種警示訊號(Warning Signs) 保持敏感:
- 「這段程式碼真的很巧妙」——通常意味著糟糕的程式碼,考慮重寫使其不再「巧妙」
- 某個類別的錯誤數高於平均——它很可能會持續如此,考慮重寫
- 異常多的缺陷暗示流程有缺陷——好的流程(架構審查、設計審查、程式碼審查)會在測試前消除大部分錯誤
- 設計度量(Design Metrics)的警示:類別成員超過 7 個、常式決策點超過 10 個、邏輯巢狀超過 3 層、高耦合、低內聚
- 重複性程式碼或在多處做類似修改——控制權未充分集中到類別或常式中
- 難以為測試案例建立腳手架——類別間耦合可能過緊
- 難以撰寫註解或命名變數——設計尚未清晰到可以編碼的程度
主動創造自己的警示。例如釋放指標後立即設為 null,使錯誤更容易被發現。編譯器警告也是字面上的警示——務必修正到零警告。
34.8 迭代,反反覆覆,一次又一次#
迭代(Iteration) 適用於軟體開發的眾多活動:
| 活動 | 迭代方式 |
|---|---|
| 需求 | 與使用者反覆推敲多個版本,直到雙方達成共識。專案失敗的常見原因是在探索替代方案前就鎖定解法 |
| 估算 | 使用多種估算技術的迭代方法比單一技術更準確 |
| 設計 | 軟體設計是啟發式過程,第一次嘗試可能可行,但不太可能是最佳解。多次不同角度的嘗試能產生單一方法所無法獲得的洞察 |
| 程式碼調校 | 很多看似會讓系統更小更快的技術,實際上反而讓它更大更慢。需要調校、量測、再調校的循環 |
| 審查(Reviews) | 在各階段插入迭代——審查通過則不需進一步迭代,未通過則返工 |
軟體工程的訣竅在於讓可拋棄的部分盡可能快速且廉價地建造——這就是在早期階段迭代的意義。在後期迭代則是「花兩塊錢做別人花一塊錢就能做的事」。
34.9 汝當分離軟體與信仰#
信條主義(Dogmatism)在軟體開發中以各種形式出現:對單一設計方法的教條式堅持、對特定排版或註解風格的不動搖信仰、對全域資料的狂熱迴避——無論哪種,都不恰當。
軟體先知#
新方法的推廣(Technology Transfer)對推進實務很重要,但要區分合理的技術轉移與兜售萬靈丹。與其追逐最新的奇蹟流行,應混合使用各種方法——嘗試令人興奮的新方法,但依靠經過驗證的舊方法。
兼容並蓄(Eclecticism)#
軟體開發不是確定性的演算法過程,而是啟發式(Heuristic) 過程,僵化的方法論不適合。有時自頂向下分解有效,有時物件導向、自底向上組合或資料結構驅動更好——你必須願意嘗試多種方法。將技術視為工具箱(Toolbox) 中的工具,用判斷力為每個問題選擇最合適的工具。教條主義立場與工具箱方法不相容。
實驗#
兼容並蓄的近親是實驗(Experimentation)。在每個準備做選擇的層次上,都可以設計對應的實驗來判斷哪種方法最有效——架構層可草擬多種架構方案、細節設計層可追蹤不同低階方案的影響、語言層可寫短程式測試不熟悉的語言特性、調校層可做基準測試驗證效果。
對所有方面保持開放心態。許多僵化方法源於對犯錯的恐懼,但全面迴避錯誤本身就是最大的錯誤。設計是精心規劃小錯誤以避免大錯誤的過程。
要點#
- 程式設計的首要目標是管理複雜度
- 開發流程對最終產品有顯著影響
- 團隊程式設計的本質是人與人的溝通,而非人與電腦的溝通;個人程式設計則是與自己的溝通
- 程式設計慣例若被濫用會比疾病更糟,但若經過深思熟慮的運用,能為開發環境增添有價值的結構
- 用問題領域而非解法的術語來程式設計,有助於管理複雜度
- 留意知識層面的警示訊號——在幾乎純粹腦力活動的程式設計中尤為重要
- 在每項開發活動中迭代越多,該活動的成果就越好
- 教條式方法論與高品質軟體開發不相容——充實你的知識工具箱,提升為每個問題選擇正確工具的能力