本章聚焦於「小規模的程式設計」——建立個別類別及其子程式的具體步驟。 其中介紹的虛擬碼程式設計流程(Pseudocode Programming Process, PPP),能減少設計與文件撰寫的工作量,同時提升兩者的品質。
9.1 建立類別和子程式的步驟概述#
建立類別的步驟#
類別的建構通常是一個迭代過程,包含以下關鍵步驟:
- 建立類別的整體設計:定義類別的職責、隱藏的「秘密」、對外介面所呈現的抽象概念,以及主要的公開方法與重要的資料成員。
- 建構類別中的每個子程式:建構個別子程式的過程中,往往會發現需要額外的子程式,這些新發現可能會回頭影響整體設計。
- 審查與測試整個類別:個別子程式完成後,應對整個類別進行審查與測試,找出單一子程式層級無法發現的問題。
flowchart TD
step1["1. 建立類別的整體設計"]
step2["2. 建構每個子程式"]
step3["3. 審查與測試整個類別"]
step1 --> step2
step2 --> step3
step2 -- "發現需調整設計" --> step1建立子程式的步驟#
較複雜的子程式適合採用系統化的方法來建構,主要活動依序為:
- 設計子程式(Design the routine)
- 檢查設計(Check the design)
- 撰寫程式碼(Code the routine)
- 檢查程式碼(Check the code)
這些步驟之間可視需要反覆進行。若品質不佳,應毫不猶豫地退回先前步驟重新來過。
9.2 如何寫好虛擬碼#
虛擬碼(Pseudocode) 是一種非正式、類似英語的表示法,用來描述演算法、子程式或程式的運作方式。PPP 定義了一套運用虛擬碼來簡化程式碼建立過程的方法。
有效虛擬碼的撰寫準則#
- 使用精確描述特定操作的類英語敘述
- 避免目標程式語言的語法元素——虛擬碼的價值在於能在比程式碼更高的層次進行設計
- 在意圖層次(level of intent)撰寫——描述做法的意義,而非如何在目標語言中實作
- 撰寫得足夠細緻,使得從虛擬碼產生程式碼幾乎是自動化的過程;若太高階則可能忽略關鍵細節
糟糕的虛擬碼會包含目標語言的指標語法(如
*hRsrcPtr)或特定函式(如malloc()),並且聚焦於「如何撰寫程式碼」而非「設計的意義」。這種虛擬碼無法轉換為有用的註解。
PPP 的好處#
| 好處 | 說明 |
|---|---|
| 簡化審查 | 審查幾行虛擬碼比審查幾十行程式碼容易得多 |
| 支援逐步精煉(Iterative Refinement) | 從高階設計逐步細化到虛擬碼,再到原始碼,在每個層級捕捉該層級的錯誤 |
| 簡化變更 | 在虛擬碼階段修改比寫完程式碼後修改成本低得多 |
| 最小化註解工作 | 虛擬碼直接轉化為程式碼中的註解,省去事後補寫註解的麻煩 |
| 容易維護 | 註解與程式碼共存,不像其他設計文件容易與實作脫節 |
9.3 透過 PPP 建立子程式#
flowchart TD
design["設計子程式"]
pseudo["撰寫虛擬碼"]
comment["虛擬碼轉為註解"]
code["填入程式碼"]
check["檢查程式碼"]
finish["收尾"]
design --> pseudo
pseudo --> comment
comment --> code
code --> check
check --> finish
check -- "品質不佳,退回重做" --> design設計子程式#
設計是建構子程式的第一步,包含以下活動:
| 活動 | 說明 |
|---|---|
| 檢查前置條件 | 確認子程式的工作已被明確定義,且符合整體設計 |
| 定義子程式要解決的問題 | 釐清子程式隱藏的資訊、輸入、輸出、前置條件(Preconditions)與後置條件(Postconditions) |
| 命名子程式 | 好的名稱應清晰且毫無歧義;若難以命名,往往代表設計不夠明確 |
| 決定如何測試 | 在撰寫子程式時就規劃測試案例 |
| 研究標準函式庫 | 檢查是否已有可重用的既有程式碼 |
| 思考錯誤處理 | 考慮所有可能出錯的情境,並有意識地選擇處理策略 |
| 思考效率 | 大多數系統中,優先確保介面良好抽象與程式碼可讀性;少數效能關鍵的系統,則依架構所訂的資源與速度預算來設計 |
| 研究演算法與資料型別 | 查閱演算法書籍,避免從零重新發明 |
撰寫虛擬碼#
- 先寫一段簡潔的標頭註解,描述子程式的用途——若難以摘要,代表需要重新理解子程式的角色
- 填入高階虛擬碼,描述子程式的邏輯流程
- 思考資料:若資料操作是子程式的重要部分,先定義關鍵資料型別
- 檢查虛擬碼:退一步思考如何向他人解釋,請人檢視幾行虛擬碼
- 反覆嘗試多種方案,在虛擬碼階段選出最佳設計再開始編碼
一旦開始編碼,就會對程式碼產生情感投入,更難捨棄不良設計。因此務必在虛擬碼階段充分迭代。
撰寫程式碼#
將虛擬碼轉化為實際程式碼的步驟:
- 撰寫子程式宣告:寫出介面定義,將標頭註解轉為程式語言的註解
- 將虛擬碼轉為高階註解:寫出子程式的首尾語句(如
{和}),將虛擬碼轉為行內註解 - 在每行註解下方填入程式碼:每段虛擬碼註解對應一個程式碼段落,通常展開為 2 到 10 行程式碼
- 檢查是否需要進一步分解:若某行虛擬碼展開出過多程式碼,可將其提取為新子程式,或遞迴地對該段再次套用 PPP
檢查程式碼#
- 心智檢查:在腦中逐一執行每條路徑,包括正常路徑、端點和所有例外狀況(可自行「桌面檢查」或與同儕進行走查/審查)
- 編譯子程式:將編譯器的警告層級設到最嚴格,消除所有錯誤訊息與警告的根本原因
- 在除錯器中逐步執行:確認每一行程式碼都如預期般運作
- 測試程式碼:使用先前規劃的測試案例進行測試,必要時建立腳手架(Scaffolding)
- 移除錯誤:若子程式在此時仍有大量錯誤,考慮重寫而非修補——修補通常代表理解不完整
專業程式設計師與業餘愛好者最大的差異之一,在於從「迷信」走向「理解」。如果你經常懷疑是編譯器或硬體的錯誤,你仍停留在迷信的領域——事實上約 95% 的錯誤源自程式設計師自身。若子程式能運作但你不知道為什麼,請務必研究到徹底理解為止。
收尾工作#
收尾檢查清單
- 檢查子程式的介面:所有輸入輸出資料是否已處理,所有參數是否被使用
- 檢查整體設計品質:子程式是否只做一件事、與其他子程式是否為鬆耦合、是否具防禦性
- 檢查變數:命名是否準確、有無未使用的物件、初始化是否正確
- 檢查語句與邏輯:是否有差一錯誤(Off-by-one)、無窮迴圈、不當巢狀、資源洩漏
- 檢查排版:空白是否恰當地呈現邏輯結構
- 檢查文件:虛擬碼轉成的註解是否仍然準確
- 移除多餘的註解:若虛擬碼註解與緊接其後的良好命名函式呼叫重複,則可移除
9.4 PPP 的替代方案#
| 替代方案 | 說明 |
|---|---|
| 測試先行開發(Test-First Development) | 在撰寫任何程式碼之前先寫測試案例,驅動設計與實作 |
| 重構(Refactoring) | 透過一系列保持語意不變的轉換來逐步改善程式碼,利用「程式碼異味」(Code Smells)來識別需要改善的區段 |
| 契約式設計(Design by Contract) | 將每個子程式視為具有前置條件與後置條件的契約 |
| 亂搞式開發(Hacking) | 若你發現自己經常寫到一半走進死胡同、在編碼途中迷失思路、或盯著螢幕不知從何下手——這些都是 PPP 能幫助你的明確信號 |
要點#
- 類別與子程式的建構是迭代過程,建構個別子程式時獲得的洞見往往會回頭影響類別的設計
- 好的虛擬碼應使用可理解的英語、避免特定程式語言的特性、在意圖層次撰寫
- PPP 是細節設計的有力工具,虛擬碼直接轉化為註解,確保註解的準確性與實用性
- 不要滿足於第一個想到的設計,在虛擬碼中反覆嘗試多種方案,選出最佳方案再開始編碼
- 在每個步驟都檢查你的工作,並鼓勵他人一同檢查,以最低的成本在最早的階段捕捉錯誤