📕 程式碼大全

Code Complete(第一版, 1993)

建構的工程數據

📐 動手之前

🎯 什麼情境該想到我

當你被催著「先開始寫再說」,但問題定義、需求、結構設計其實還沒到位時——這張卡是用來在動手前擋下來,並且用成本數字說服你的老闆為什麼要擋。

書裡的比喻:如果基礎打不好,你最多只能防止失敗,談不上做好。

「如果你想做一件精美的首飾,那麼就得用鑽石作原料。如果你用的是磚頭,那你所能得到的最好結果不過是塊漂亮的磚頭而已。」(p.12)

上游沒做好,下游再認真也救不回來——書用「軟體食物鏈」講這件事:結構設計吃掉需求分析、詳細設計以結構設計者為食、而他自己又成為編碼者的食物;需求定義一旦被污染,程式設計師是食物鏈最後一環,吃下所有上游的毒(p.14–15)。


⚙️ 怎麼用

步驟 0:先把成本數字準備好(這是你唯一的談判籌碼)

動手前先記住這四組數字,被要求「馬上開始寫」時直接拿出來:

主張倍數出處
在需求定義/結構設計階段改,vs 在建構與維護階段改成本低 50 到 100 倍Boehm 和 Pappecio,1988(p.15)
在詳細設計、編碼或單元測試階段消除錯誤,vs 在系統測試與功能強化階段成本低 10 到 100 倍Fagan,1976(p.15)
一個需求錯誤,在後面各階段才修正的放大倍數總體結構 、編碼 10×、單元或系統測試 20×、驗收測試 50×、維護 100×IBM、GTE、TRW(p.17)
全書小結版本建構工作後更改需求,比在需求分析階段改高 20 到 100 倍p.30

錯誤引入時間 vs 發現時間的相對修復成本(表 3-1,Robert Dunn 總結,p.15):

錯誤發現時間 \ 引入時間需求分析細節設計編碼
需求分析1
細節設計21
波動測試521
結構測試1552
功能測試25105

「在需求分析階段引入的錯誤,如果馬上發現並消除所耗費的成本是 1000 美元的話,那麼如果到了功能測試階段才發現和消除,耗費的成本則會高達 25000 美元。」(p.15)

一句話版本:一旦引入錯誤,就盡早發現和消除它。錯誤在軟體食物鏈中存留的時間越長,危害傳播得越遠(p.15)。

步驟 1:問題定義關(p.16)

  • 有一份簡短的問題說明,而且它聽起來像一個問題,不像一個解決方案
    • 好:「我們無法跟上指令系統」。壞:「我們需要最佳化資料入口系統以便跟上指令系統」。
  • 從使用者觀點、用使用者的語言寫的,沒有用電腦技術術語(除非問題本身就是關於電腦,例如編譯太慢)。
  • 確認過最好的解法「可能根本不是一個電腦程式」——書裡的例子:與其寫一支年度利潤報表程式,不如讓祕書把四季利潤用計算機加起來。
  • 沒過的代價是雙重的:你浪費時間解了一個錯的問題,而真正的問題仍然沒解決。

步驟 2:需求關(p.16–19)

先確認為什麼要有正式需求(p.16–17):讓使用者而不是程式設計師決定系統功能、避免爭議(吵起來就查文件)、把開發開始後的改動降到最小。

拿書中的檢查表逐條問(p.19,摘關鍵幾條):

內容

  • 所有輸入都定義了嗎?含來源、精度、取值範圍、頻率。
  • 所有輸出都定義了嗎?含目標、精度、取值範圍、頻率、格式。
  • 所有硬體/軟體介面、通訊介面(含握手、錯誤檢查、通訊約定)都定義了嗎?
  • 從使用者觀點定義了必要操作的反應時間嗎?處理時間、資料傳輸率、系統吞吐能力呢?
  • 保密級別、可靠性(含出錯後果、要保護的關鍵資訊、錯誤測試與恢復策略)規定了嗎?
  • 所需最大記憶體、最大儲存容量規定了嗎?
  • 維護性規定了嗎(對運行環境、精度、效能、與其他軟體介面變化的適應能力)?
  • 相互衝突的設計之間的折衷原則規定了嗎(例如堅固性 vs 準確性)?
  • 制定了系統成敗的標準嗎?

完善性

  • 開發開始前暫時得不到的資訊是什麼?不夠完善的區域有標示出來嗎?
  • 需求中有沒有哪一部分讓你不安?有沒有根本不可能實現、只是為了取悅老闆和使用者才加進來的內容?

品質

  • 是用使用者的語言寫的嗎?使用者也這樣認為嗎?
  • 條目之間避免衝突了嗎?有沒有不小心規定了設計工作
  • 每一條都能追溯到產生它的環境嗎?
  • 每一條需求都可以作為測試依據嗎?可以針對每一條獨立測試嗎?
  • 對可能的改動作出規定了嗎(含每項改動的可能性)?

步驟 3:結構設計關(p.20–26)

為什麼它是先決條件:結構設計的品質決定系統概念上的完整性,而這又決定最終品質。在建構階段修結構錯誤,比修需求錯誤省時,但比修編碼錯誤耗時得多(p.20)。

逐項確認結構設計有沒有交代(p.20–25):

  • 程式的組織形式:有總體概括描述;主要模組都定義了(模組 ≠ 常式,是完成某一高階功能的常式組合);需求中列出的每一項功能都至少有一個模組覆蓋;每個模組做什麼、模組間介面、誰可以呼叫誰、傳送什麼資料,都明確定義。並且說明了為什麼選這個組織形式(考慮過的替代方案)——設計理由對維護性和設計本身同樣重要(Rombach 1990)。
  • 變動策略:說明系統如何應付變動;最可能的變動同時是最容易實現的變動;每一個單一變動只涉及數量有限的幾個模組;有延緩變動的手段(例如用表驅動而非手工編碼、把表放外部檔案而非編碼在程式裡,改了不必重新編譯)。
  • 購買而不是建造的決定:要用現成軟體就說明如何讓它適應新需求,並證明改得動。(Boehm 1984:重用舊軟體是提高生產率的首要因素;Caper Jones 1986:購買的程式碼從 0 上升到 50%,生產率可提高一倍。)
  • 主要資料結構:列出使用的主要檔案、表、資料結構,附替代方案與選擇理由;不允許一個以上的模組直接存取資料結構,除非透過存取常式;用資料庫就規定其組織形式與內容;遵循資料守恆定律——每一個進入的資料都應該出去,不出去就沒必要進來。
  • 關鍵演算法:描述或指出演算法,說明考慮過哪些方案、為什麼選它、基於什麼假定(書例:為什麼用堆排序而不是快速排序或插入排序)。
  • 主要物件(物件導向系統):規定每個物件的責任、彼此如何互動,含排序層次、狀態轉換、物件一致性。
  • 通用功能:使用者介面(命令結構、輸入格式、選單;且要模組化到可以把互動式介面換成批次介面,這在單元與子系統測試特別有用)、輸入/輸出(查詢方式;在區域/記錄/檔案哪個層次檢查 I/O 錯誤)、記憶體管理(正常與極端情況的估計)、字串儲存(估計字串佔用的記憶體;壓縮;不改程式碼即可維護字串;譯成外文時對程式碼影響最小)。
  • 錯誤處理策略(p.23–24)——書說有人估計程式中 90% 的程式碼是為了應付例外的錯誤處理或內務處理,只有 10% 在處理正常情況,所以這一項一定要在結構階段講清楚。五個必答問題:
    1. 錯誤處理是糾正還是僅僅測試錯誤?(無論哪種,都要提醒使用者)
    2. 錯誤測試是主動的還是被動的? 主動=積極預防,例如檢驗使用者輸入是否合法;被動=無法迴避時才反應。
    3. 程式怎樣對付錯誤?立刻拋棄出錯資料、進入錯誤處理狀態、還是全部處理完再通知使用者?
    4. 處理錯誤訊息的約定是什麼?沒有統一策略,使用者介面就會像迷宮一樣忽東忽西。
    5. 在哪一層次處理錯誤?發現處就地處理、交給錯誤處理常式、還是交給更高層常式?另外:每個模組驗證輸入合法性的責任級別有多高?
  • 堅固性(robustness)(p.24):
    • 裕度設計(over-engineering):明確表述要求的系統裕度有多大。結構設計規定的裕度往往比需求定義大,因為系統由許多部分組成——軟體鏈條的強度不是由最薄弱的一環決定,而是由所有薄弱環節的乘積決定。講清楚裕度級,才不會有的地方裕度過大、有的過小。
    • 斷言(assertions):結構中要規定斷言的使用程度。斷言是放在程式碼中、執行時可使其自檢的可執行語句。

      「比如,系統假定使用者資訊檔案永遠不會超過 5000 記錄行,那麼程式中可能會包含一段說明這個假定的斷言。只要這個檔案不超過 5000,那麼斷言就保持沉默,而一旦斷言發現此檔案超過了 5000 個記錄行,那它就會聲稱已發現了一個錯誤。」(p.24)
      「為了在程式中加入斷言,你必須知道在設計系統時所做的假設,這也是在結構設計中應闡明採用假設的原因之一。」(p.24)

    • 容錯性(fault tolerance):指明期望的容錯類型——返回並重新開始、用輔助程式碼代替基本程式碼、投票演算法(三種方法各算一次再比較)、用假想值代替錯誤結果;其他還有部分運行、功能降級、關閉自己、自動重新開始。
  • 效能:目標含速度與記憶體使用;結構設計要估計並解釋為什麼這些目標可以達到;規定每一個模組的時間與儲存空間預算。
  • 整體品質:概念完整性是最重要的(Brooks 1975《The Mythical Man-Month》);看到它應該為方案的自然和簡單而折服;每個決定的動機都要說清楚(當心「我們過去一直是這麼幹的」);恰好落在過分定義與定義不足的分界線上;沒有任何部分是只為取悅老闆而加的。

步驟 4:語言選擇關(p.26–27)

  • 確認團隊對這個語言的熟悉度。TRW 資料:同水平的兩人,一個用了三年的語言 vs 陌生語言,前者效率高 30%;IBM 調查:對某語言經驗豐富者,效率是無經驗者的 三倍(Walston 和 Felix 1977)。
  • 確認語言層級。Brooks 1987:Pascal 和 Ada 的效率、可靠性、簡單性、可懂性是組合語言與機器語言等低階語言的 5 倍
  • 小心「用 A 語言寫 B 語言的程式」——書中的典型故事:一群 Fortran 出身的人用 Pascal 編譯器寫出變形的 Fortran,用 goto 和全域資料歪曲了 Pascal,卻把 Pascal 豐富的控制和資料結構棄之不用(Hanson 1984、Yourdon 1986)。

步驟 5:編程約定關(p.29)

  • 在建構工作開始之前,一定要寫明將採用的編程約定(變數與常式命名、格式約定、註釋約定),而且要寫得非常詳盡,使得在編程過程中無法對其進行改動。
  • 理由(p.30 小結):在建構工作結束後再改程式碼來滿足約定,幾乎是不可能的。

步驟 6:時間預算關(p.29–30)

  • 排程裡有沒有先決條件的時間?「一個運行良好的專案通常把 20~30% 的時間用於先決條件」——而且這 20~30% 不包含詳細設計,因為詳細設計是建構活動的一部分。
  • 需求不穩定又被要求報時程時:先做完需求分析,再估計專案其餘部分需要多少時間

    「在建築中,在知道要建什麼之前,就進行工程預算顯然是荒謬的。」(p.30)

步驟 7:如果檢查沒過,這樣處理(p.17–18)

  1. 停下來回去補。「如果你在開車從芝加哥到洛杉磯的途中,發現自己到了紐約市郊,那麼停下車來看一下地圖是浪費時間嗎?」
  2. 讓每個人知道變更需求的代價。標準台詞:「你的想法不錯,但由於它不在需求文件之中,我想先做一個變動後的進度和成本估計,然後我們再決定是立刻採用還是以後再說。」——「時間進度」和「成本」這兩個詞往往比咖啡和潑冷水更管用,會把許多「立刻採用」變成「最好採用」。
  3. 建立一套更改控制過程(正式的變更審查委員會)。使用者改主意很正常,改到你跟不上才不正常。
  4. 用開發方法容納變動:原型化開發、漸進開發(按階段發布、短週期、拿回饋再調整)。
  5. 放棄專案。即使不能真的砍掉,也想一想砍掉會有什麼差別。

被老闆逼著立刻開始編碼時(WISCA/WIMP 現象:「為什麼 Sam 沒有正在寫程式?」)的四個替代做法(p.13–14):平靜地拒絕按錯誤順序工作/假裝在寫程式(把舊的程式清單放桌角,埋頭寫需求與構想文件)/教育老闆技術專案的開發方式/另找一份工作。


🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 這章不教你怎麼做需求分析或結構設計,只教你怎麼確認它們做完了、品質夠不夠當建構的基礎。要學怎麼做,書中推薦 DeMarco、Yourdon、Hatley & Pirbhai、Shlaer & Mellor、IEEE Std 830-1984(p.19–20)。
  • 「穩定的需求」是神話(p.17)。IBM 調查:一個典型的一百萬字需求分析,開發過程中約 25% 的內容要變動。使用者是隨專案進行才逐漸弄清自己要什麼的——「一個從不變更需求的計畫,事實上是一個對使用者的需求不予理睬的計畫。」 所以本卡的目的不是凍結需求,是讓變動發生在最便宜的階段、並且被控制
  • 成本倍數在小專案會縮水:「在較小規模的計畫中,在維護階段修正錯誤的放大因子可能是 20 而不是 100,因為這時管理費用較低。」(p.17)
  • 先決條件隨專案規模與正式程度而變(3.8,p.30)。書中兩張檢查表都明說:非正式專案裡有些條款根本不必考慮,大型正式專案才建議逐條仔細考慮(p.18、p.25)。細節見書中第 21 章。
  • 「好的結構應與機器和語言獨立」有例外:如果程式專門是為某一機型或語言設計的,這條不適用(p.25)。
  • 如果你已經很熟軟體工程生存期循環,書自己說可以跳過這章直接進下一章(p.12)。
  • 也不要走到另一個極端:結構設計應該「恰好在過分定義和定義不足的分界線上」,不能以犧牲某一部分為代價來重視另一部分(p.25)。

🔗 相關工具

名稱:第一版沒有「PPP」這個名字

我逐字查過《程式碼大全》第一版(1993 簡體中譯)第 4 章「建立子程序的步驟」(p.31–44)

  • 全章沒有出現過「偽代碼」這三個字,也沒有 “Pseudocode Programming Process” 或縮寫 PPP
  • 第一版把這個東西叫 PDL(程序設計語言,Program Design Language),把流程叫 「PDL 到程式碼流程」(p.33、p.43、p.44)。
  • 「Pseudocode Programming Process (PPP)」是第二版(2004)才確立的名稱,不是本書(第一版)的用語。舊卡標題上的 PPP 已撤下。

本卡只依據第一版第 4 章寫成,每條標書內頁碼;引用照原文,只做簡→繁與明顯 OCR 錯字修正;書中的「子程序」在本卡統一寫成常式

🎯 什麼情境該想到我

當你要寫一個新常式/函式、卻盯著空白畫面不知從何下手時。

第 4 章的定位(p.31):「本章詳細講述了在建立一個常式時的典型步驟……本章的中心內容是如何編寫小規模的程式,以及編寫對各種規模專案都十分關鍵的程式的特定步驟。」


⚙️ 怎麼用

第 0 步:先知道整體順序(4.1,p.31)

「在建立程式過程中引入的許多低層次細節問題,並不需要按某一特定順序來進行,但是一些主要活動——設計程式、檢查程式、常式編碼、檢查程式碼,則應該按圖 4-1 的順序來進行。」(p.31,圖 4-1 在 p.32)

也就是三大段:4.3 設計常式 → 4.4 常式編碼 → 4.5 檢查常式


PDL 該寫成什麼樣(4.2,p.31–32)★

PDL 是 Caine、Farber、Gordon 共同開發的(書中 OCR 作「Came、Father 和 Gordon」),1975 年發表後曾作過重大修改(p.31)。書強調「PDL 之間的好壞是有判別的」,並給四條方針(原文照列,p.31–32):

  1. 用模擬英語的語句來精確描述每一個特定操作。
  2. 避免使用最終程式語言的語句。

    「PDL 使你比在程式碼稍高階的層次上進行設計工作。當使用程式語言進行建立時,就又回到了低層次上,從而得不到由於在高層次上進行設計的好處,而且會受到不必要的程式語言語法規則的限制。」(p.31)

  3. **在設計意向這一層次上寫 PDL。**說明方法的意義,而不是描述如何用目標語言實現。
  4. 在足夠低的層次上寫出 PDL,它幾乎可以自動生成程式碼。「如果 PDL 寫得太簡略,可能會在編碼過程中忽略問題細節。應該精確地使用 PDL 以方便編碼。」(p.32)

書給的壞 PDL(p.32,違反了上面幾乎所有原則):

Increment resource number by 1
allocate a dlg struct using malloc
if malloc() returns NULL then return 1
invoke OSrsrc_init to initialize a resource for the operating system
*hRsrcPtr = resource number
return 0

「之所以稱之為一個錯誤使用 PDL 的典型,是因為它使用了像 *hRsrcPtr 這種特定的 C 語言指標標誌和 malloc() 這個特定的語言函數,即它採用了程式碼語句。這段 PDL 的中心是如何寫程式碼,而不是說明設計意義。」(p.32)

書給的好 PDL(p.32–33,同一個操作):

Keep track of current number of resources in use
If another resource is available
    Allocate a dialog box structure
    If a dialog box structure could be allocated
        Note that one more resource is in use
        Initialize the resource
        Store the resource number at the location provided by the caller
    Endif
Endif
Return TRUE if a new resource was created; else return FALSE

「它完全是用自然語言寫成的,沒有使用任何目標程式語言語句。在第一段 PDL 中,它只能用 C 語言來實現,而第二段卻並沒有限制所使用的語言。同時,第二段 PDL 也是在意圖層次上寫成的。」(p.33)

為什麼值得這樣寫——書列的五個好處(p.33):

好處書怎麼說
評審更容易「不必檢查原始碼就可以評審詳細設計」,並減少評審程式碼本身的工作
幫助實現逐步細化結構設計 → 細化為 PDL → 細化為原始碼;每次細化之前都檢查設計,「在每個層次上都可以發現當前層次的錯誤,從而避免影響下一層次的工作」
變動容易「幾行 PDL 改起來要比一整頁程式碼容易得多。你是願意在藍圖上改一條線還是在房屋中拆掉一堵牆?」——關鍵是「在投資最少時找出錯誤,以降低改錯成本」
極大減少註釋工作量一般流程是先寫程式碼再加註釋;PDL 流程中「PDL 本身就是註釋」,而「從程式碼到註釋的花費要比從註釋到程式碼高得多」
比其他形式的設計文件容易維護其他方式下設計與程式碼是分隔的,改了一個另一個就對不上;PDL 流程中「只要直接維護註釋,那麼關於設計的 PDL 文件就是精確的」

書自己的定位(p.33):「PDL 並不是詳細設計的唯一工具,但是 PDL 和 PDL 到程式碼流程的確是有用的工具。」


4.3 設計常式(p.33–36)——書列的完整步驟

書用 RecordErrorMessage()(依錯誤代碼輸出錯誤訊息)當貫穿全章的例子。設計階段的步驟(原文小標,見圖 4-2,p.34):

  1. 檢查先決條件(p.34)——動手前先確認這個常式的工作任務是否已定義、是否和整個結構設計融為一體、是否真的會被呼叫;「至少,在專案的要求定義中就涉及到它」。→ 見 工具-建構的先決條件
  2. 定義這個常式將要解決的問題(p.34)——結構設計至少要指出這四項:
    • 這個常式將要隱含的資訊
    • 這個常式的輸入
    • 這個常式的輸出,包括受到影響的全域變數
    • 這個常式將如何處理錯誤
  3. 給常式命名(p.34–35)——「好的常式名字往往是一個高品質軟體的標誌之一」;命名困難通常代表你對它的功能還不清楚。

    「一個模稜兩可的名字就像是一個在進行競選辯論的政治家,似乎他在說著什麼,可是當你仔細聽時,又分辨不出他的話到底有什麼意義。」(p.34–35)
    「如果產生一個模稜兩可名字的原因是模稜兩可的結構設計,那麼就應注意這個危險信號,這時應追回去改進結構設計。」(p.35)

  4. 決定如何測試常式(p.35)——「在編寫常式時,最好能同時考慮如何測試。這對進行單元測試工作是很有益處的。」
  5. 考慮效率(p.35)——分兩種情形:效能不是主要的 → 做成高度模組化、可讀性強,日後好替換;效能重要 → 結構設計應規定速度與記憶體額度,照指標設計即可。

    「除了以上指明的情況以外,不必浪費精力去考慮個別常式的執行效率。最佳化的收益主要來自高層次設計,而不是個別常式,只有在高層次設計某方面有缺陷時,才需要進行微觀最佳化,而這點只在程式全部完成時才會知道。除非必要,不要浪費時間進行增量改進。」(p.35)

  6. 研究演算法和資料結構(p.35)——「同時提高編碼品質和效率的最有效辦法是重新使用好的程式碼」;與其自己發明別人已寫過博士論文的東西,不如花幾分鐘瀏覽演算法論著。
  7. 編寫 PDL(p.35–36)——從抽象到具體
    • 最抽象的部分是最開始的註釋,先寫一個關於這個常式目的的精確說明。「如果在編寫這部分說明時感到困難,那說明需要對這個常式在整個軟體中的地位和作用作出更深刻的理解。」(p.35)

    • 然後才寫高層次 PDL。書的成品(p.35–36):

      This routine outputs an error message based on an error code
      supplied by the calling routine. The way it outputs the message
      depends on the current processing state, which it retrieves
      on its own. It returns a variable indicating success or failure.
      
      set the default status
      look up the message based on the error code
      if the error code is valid
          determine the processing method
          if doing interactive processing
              print the error message interactively and declare success
          else doing batch processing
              if the batch message file opens properly
                  log the error message to the batch file,
                  close the file, and declare success
      else the message code is not valid
          notify the user that an internal error has been detected
      

    「應該注意的是這個 PDL 是在一個相當高的層次上寫成的。它使用的不是程式語言,而是用自然語言來精確表達設計思想的。」(p.36)

  8. 考慮資料(p.36)——「如果資料操作是程式的主要部分,那麼在考慮程式的邏輯結構之前,考慮主要資料是必要的。」
  9. 檢查 PDL(p.36)——請別人幫忙看一下或聽一下你的說明

    「也許你認為請別人看一個只有 11 行的 PDL 是很愚蠢的,但你會對這樣做的結果感到驚奇。PDL 使假設和高層次錯誤比程式語言程式碼容易被發現。人們往往更願意檢查一個只有幾行的 PDL,而不願去檢查一個有 35 行的 C 或 Pascal 常式。」(p.36)
    「要確認對常式做什麼和將怎樣做已經有了清楚透徹的了解。如果在 PDL 這一層次上對這點還沒有概念上的了解,那麼在編碼階段了解它的機會還有多少呢?」(p.36)

  10. 逐步細化(p.36)——

    「在開始編碼之前,要盡可能多使用 PDL 嘗試一些想法。一旦開始編碼,就會對所寫的程式碼產生愛惜之情,這時,要再想把它扔掉重新開始是非常困難的。」(p.36)
    「通常的思想是逐步細化用 PDL 寫成的常式,直到可以在每行 PDL 語句下面添加一行程式碼而成為常式為止……要不斷地細化 PDL 並對其作出進一步說明,直到你看到這樣做是在浪費時間時,再開始實際的編碼工作。」(p.36)


4.4 常式編碼(p.37–42)——PDL 怎麼變成程式碼

書列的實現步驟(見圖 4-3,p.37):

  1. 書寫常式說明(p.37)——先寫介面語句(程序/函數宣告),並把原來的抽象說明用目標語言的註釋形式寫在 PDL 之上。「這時是指出介面假設的好時機。」(p.38)
  2. 把 PDL 轉變成高層次註釋(p.38)——用 Pascal 的 begin/end 或 C 的 {} 把每行 PDL 包成註釋。

    「這時,常式的特點已經非常明顯了,設計工作已經結束了,沒看見任何程式碼,但已經知道常式如何工作了。把 PDL 轉換成程式語言程式碼是一件機械、自然、容易的工作。如果你不覺得是這樣,那麼還需要進一步細化 PDL,直到有這種感覺為止。」(p.38–39)

  3. 在每一行註釋下面填上程式碼(p.39)——

    「這有點像給報紙排版。首先畫好輪廓線,然後再把每一篇文章填到空格中,每一個 PDL 註釋行都相當於給程式碼畫的輪廓線,而程式碼相當於文章。」(p.39)

    • 書明講註釋要留著,不是刪掉(p.41):「如果是在事後進行註釋,那麼,用兩個註釋行來註釋兩行程式碼就不必要了。但是,採用目前這種方法,注重的是註釋的字面內容而不是它註釋了多少行程式碼。現在,註釋行已經存在了,所以還是將其保留。」「保留了註釋以便提供一個關於程式碼的高層次解釋。」
    • 書也順帶指出這個流程的價值(p.41):從 5 行要求定義 → 12 行初始 PDL → 一個較大的常式,「即使要求定義是很詳盡的,常式的建立還是需要在 PDL 和編碼階段進行潛在的設計工作。這種低層次的設計工作正是為什麼編碼不是一件瑣碎事的原因」。
  4. 非正式地檢查程式碼(p.41)——每填完一塊就「盡力想一下什麼因素可能破壞目前的塊,然後證明這種情況不會發生」;整個常式實現完再停下來檢查一次,因為「某些重大問題在常式實現之前是不會出現的」。
  5. 進行收尾工作(p.41–42)——六項確認(原文照列):
    • 檢查常式的介面:所有輸入輸出資料都作出了解釋、所有參數都用到了(詳見 5.7 節)
    • 檢查通用設計品質:只完成一項任務且完成得很好、「與其他常式交叉是控制不嚴的表現」、應該採用了預防錯誤的設計(詳見第 5 章 → 工具-防禦式編程
    • 檢查常式的資料:不精確的變數名、沒有使用的資料、沒有說明的資料
    • 檢查常式的控制結構:無限迴圈、不適當的巢狀
    • 檢查常式的設計:確認已說明了運算式、參數表和邏輯結構
    • 檢查常式的文件:「確認被翻譯成註釋的 PDL 仍然是精確的」,檢查演算法描述、介面假設與非顯式相依的文件資料
  6. 按需要重複步驟(p.42)——

    「如果程式的品質很差,請返回 PDL 階段。高品質程式是一個逐步的過程,所以在重新進行設計和實現活動時,不要猶豫不決。」(p.42)


4.5 檢查常式(p.42–43)

「在設計並實現了常式之後,建立活動的第三個主要步驟是進行檢查」——因為非正式檢查與收尾「不能完全保證」正確性,漏掉的錯誤只會在後面測試時才被發現,那時成本很高(p.42)。

  1. 在心裡對常式進行查錯處理(p.42)——除了前面的非正式檢查與收尾,另一方法是在心中執行每一個路徑(這個困難「也是造成很難保持常式小規模的原因之一」)。要檢查到每一個規定路徑、中止點與所有例外情況。自己做叫「桌面檢查」;與同事一道做叫「同事評審」「過一遍」或「視察」。

    「業餘愛好者與職業程式設計師之間的最大區別就是迷信還是理解……只有 5% 的錯誤是由編譯程式、硬體或者是作業系統引起的(Brown and Sampson 1973;Ostrand and Weyuker 1984)。進入理解境界的程式設計師總是懷疑自己的工作,因為他們知道 95% 的錯誤出自這裡。」(p.42)
    「沒有僅僅因為有效便是正確的東西。如果你不知道為什麼它是有效的,那麼往往它是無效的,只不過你沒有發現罷了。」(p.42)

  2. 編譯常式(p.42–43)——書要你刻意晚一點才編譯

    「一旦開始編譯,那麼你腦袋裡的碼錶便開始滴答作響了,在第一次編譯之後,你就開始不停地想:下次編譯一定讓它全對。結果,在這種『就只再編譯一次』的壓力下,做了許多匆忙的、更易產生錯誤的修改,反而浪費了更多的時間。所以,在確信常式是正確的之前,不要急於開始編譯。」(p.42)
    編譯時的兩條方針(p.43):把編譯器的警告級別調到最高消除所有編譯器指出的錯誤和警告的原因(「大量的警告往往意味著程式碼品質不高」)。

  3. 使用電腦來檢查常式錯誤(p.43)——放進除錯器逐行執行,確保每一行都按預期執行;再用設計階段就想好的測試用例測;必要時搭鷹架(僅測試階段用、不進產品的支撐程式碼)。
  4. 消除常式中的錯誤(p.43)——

    「如果發現程式中的錯誤異乎尋常的多,那就重新開發一個,不要試圖修補它。修補往往意味著不充分的理解,而且肯定會在現在和將來產生更多的錯誤。」(p.43)


🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意 / 什麼時候不適用

  • 偽代碼要描述「做什麼」而非「怎麼用語言寫」——這是書的第 3 條方針「在設計意向這一層次上寫 PDL」(p.31)與小結(p.44):「首先要用易懂的自然語言,避免拘泥於某種程式語言,其次要在意向層次上寫 PDL,描述設計做什麼而不是如何做。」
  • 但也不能寫太高(第 4 條方針,p.32):「如果 PDL 寫得太簡略,可能會在編碼過程中忽略問題細節。」判準是「幾乎可以自動生成程式碼」。
  • 舊卡「保留有價值的偽代碼當意圖說明,其餘刪去」已修正:第一版沒有叫你刪多餘註釋,反而明說「註釋行已經存在了,所以還是將其保留」(p.41)。書唯一的但書在小結:「可以把 PDL 直接翻譯成註釋,但要注意保證註釋是精確而有用的」(p.44)。
  • 舊卡「回頭檢查、必要時重構」中的「重構」已改寫:第一版第 4 章沒有 refactoring 的概念,它的說法是**「按需要重複步驟……請返回 PDL 階段」**(p.42)與「錯誤異乎尋常的多就重新開發一個,不要修補」(p.43)。
  • PDL 不是萬能,也不是唯一(p.33):「PDL 並不是詳細設計的唯一工具。」書只主張它「是有用的工具。不妨試一下」。

隨手用的檢查表(4.5.1「建立子程序」檢查表,p.43–44,原文照列)

  • 是否檢查過先決條件已經滿足了?
  • 定義常式將要解決的問題了嗎?
  • 結構設計是否足夠清楚,使得你可以給常式起個好名字?
  • 考慮過如何測試常式了嗎?
  • 是否從模組化水平或者滿足時間和記憶體要求角度考慮過效率問題?
  • 是否查閱過參考書,以尋找有幫助的演算法?
  • 是否用詳盡的 PDL 設計常式?
  • 在必要時,是否在邏輯設計步驟前考慮了資料?
  • 是否檢查過 PDL,它很容易理解嗎?
  • 是否注意到了足以使你返回到結構設計階段的警告(使用了全域資料、更適合其他常式的操作,等等)?
  • 是否使用了 PDL 到程式碼流程,是否把 PDL 作為編碼基礎並把原有的 PDL 轉為註釋?
  • 是否精確地把 PDL 翻譯成了程式碼?
  • 在作出假設時,驗證它們了嗎?
  • 是從幾個設計方案中選擇了最好的,還是隨意選擇了一個方案?
  • 是否徹底理解你的程式碼?它容易理解嗎?

🔗 相關工具

  • 工具-建構的先決條件(同書)——設計常式的第一個步驟就叫「檢查先決條件」(p.34),第 4 章的〈相關章節〉也直接指向第 3 章。專案層級的動手前準備做完了,才輪到單一常式層級的動手前準備。
  • 工具-軟體評審與檢查(同書)——PDL 好處第一條是「PDL 可以使評審工作變得更容易……不必檢查原始碼就可以評審詳細設計」(p.33);4.5 節列的「桌面檢查/同事評審/過一遍/視察」(p.42)正是評審卡展開的內容。
  • 工具-防禦式編程(同書)——雙向:4.4 收尾工作要求確認常式「應該採用了預防錯誤的設計」(p.41);而 5.6 節反過來把「在編碼前先寫好 PDL」列為優先於防錯性編程的預防手段(p.61)。
  • 工具-程式碼調校(同書)——4.3「考慮效率」是這條原則在常式層級的第一次出現:「不必浪費精力去考慮個別常式的執行效率。最佳化的收益主要來自高層次設計……除非必要,不要浪費時間進行增量改進」(p.35)。要調校時再去看調校卡的量測紀律。
  • 工具-專案規模的影響(同書)——第 4 章自陳「本章的中心內容是如何編寫小規模的程式,以及編寫對各種規模專案都十分關鍵的程式的特定步驟」(p.31);這套步驟本身不隨規模改變,但套用時的正式程度與規模有關,見規模卡。
  • 工具-一次只做一件事編寫可讀程式碼的藝術)——對照:都是「先把意圖切乾淨、再寫語法」。
  • 工具-設計訣竅(HtDP 的系統化設計)——對照:另一套「先寫合約與範例、再填本體」的模板化設計流程。
  • 回到來源:程式碼大全(第一版,第 4 章「建立子程序的步驟」,p.31–44)

🧠 對抗複雜度

🎯 什麼情境該想到我

當你覺得一段程式「看不懂、改不動、一動就爆」時。這張卡不是口號,是可以動手算出數字、再照數字決定要不要重新設計的判準。

書把複雜性的討論放在兩個地方:17.7「控制結構和複雜性」(p.269–271)給度量與判讀,第 7 章「高階結構設計」(p.92–111)把低複雜性當成設計品質的評估項目。全章小結只留一句:

「降低複雜性是編寫高品質的程式碼的關鍵。」(p.271)

而 17.7 開頭說明為什麼要盯著控制結構:

「注意控制結構的一個原因是,它們對於克服程式的複雜性有很大貢獻。不用控制結構,會增加程式的複雜性。用得好則能降低複雜性。」(p.269)

書給複雜性的操作型定義(p.269):

「衡量一個程式複雜性的方法是,若要理解程式你得一次連續在腦中記住多少目標程式語句。」

同一段還說:結構的處理是編程中最困難的方面,這也是程式在處理「快速中斷」(quick interruption)時手忙腳亂的原因——相當於一個雜技員手上拿著東西的同時還要不斷向空中拋三個球(p.269)。

複雜性主要來自控制流,但不只控制流(p.269):

「Tom McCabe 在一本書中認為一個程式的複雜性就決定於它的控制流。別的研究人員在此之外還確認了別的影響複雜性的因素,但都承認,控制流若不是最大的,起碼也是影響複雜性的最大原因。」

那些「別的因素」書有列出來(17.7.3,p.271):用到資料的次數、控制結構中的巢狀層次、程式碼的行數、變數連續出現的程式行行數、輸入輸出點的數目;另有研究人員把以上各方法綜合起來。

為什麼值得花力氣(p.270):控制流的複雜性和低可讀性、頻繁出錯緊密聯繫在一起(McCabe 1976、Shen et al. 1985)。William T. Ward 在 Hewlett-Packard 用 McCabe 的複雜性度量標準研究可讀性(1989):一個 77,000 行的程式最終錯誤率是每千行 0.31 個錯,另一個 125,000 行的程式是每千行 0.02 個錯;Ward 發現,由於這兩個程式的複雜性較低,它們的出錯數比 HP 其他程式都低。

Dijkstra 二十年前就意識到複雜性的危險(p.269–270):

「一個聰明的程式設計師總是清楚地知道自己的腦力容量有限,因此他得十分小心謹慎地完成編程任務。」(1972)

書接著補上關鍵的一句:這並不意味著為了處理很複雜的問題你得增大你的腦力,而是說你得想辦法盡可能降低複雜性(p.270)。


⚙️ 怎麼用

步驟 1:先算「決策點」數目(表 17-2,p.270)

決策點(decision point,原書譯「決定點」)是書中最有影響的數字技巧,出自 Tom McCabe。算法就三條:

  1. 從 1 開始一直往下通過程式。
  2. 遇到下列關鍵詞或其同類的詞加 1if while repeat for and or
  3. case 語句中每一種情況都加 1;如果 case 語句沒有預設(default)情況,再加 1

書自己的例子(p.270):

if ( ( ( Status = Success ) and Done ) or
     ( not Done and ( NumLines >= MaxLines ) ) ) then …

從 1 算起,遇 if 得 2,遇 and 得 3,遇 or 得 4,又遇一個 and 得 5——這段共 5 個決策點

步驟 2:照書的門檻判讀(p.270)

決策點數目書的判讀你該做什麼
0~5程式可能很好放著
6~10得想辦法簡化程式進步驟 3 挑手法
10 以上得把部分程式碼寫成常式並在原程式呼叫進步驟 3,優先「提取成常式」

這裡有一句最容易被跳過、但決定你怎麼理解這個方法的話(p.270):

「把部分程式寫成常式並不能減少整個程式的複雜性,它僅僅是把決策點轉移到別的地方,但它卻降低了一次涉及到的複雜性。既然你的目的是降低一次要在你腦中考慮的問題數目,因此編成常式降低複雜性的方法是有幫助的。」

也就是說:這個度量從頭到尾衡量的是「一次」要在腦中裝多少東西,不是全系統的總量。切分不會讓總量變少,但會讓每一次變少——這就是目的。

檢查表把它變成一個要交代的問題(17.7.4,p.271):

「如果程式的決策點數超過 10,有什麼正常理由不重新設計它嗎?」

步驟 3:超標時的四種具體手法(17.4 防止危險的深層巢狀,p.258–264)

先看巢狀層數的判準(p.258):Noam Chomsky 和 Gerald Weinberg 的研究表明,很少有人理解巢狀超過三層的 if 語句,許多研究者也認為應當避免巢狀超過 3 到 4 層

  1. 重新組合測試條件來簡化巢狀 if(p.258–259):把外層條件併進內層判斷(if (Error == None && PrinterRoutine != NULL && SetupPage())),減少層數。書自己標了代價(p.260):把巢狀次數從 4 減到 2 是了不起的改進,能提高可讀性,但再往下減就得寫出很複雜的測試條件了,要考慮代價是否值得
  2. 把巢狀 if 改成一串 if-then-else(p.260–261):深決策樹(Qty > 10 裡包 > 100 再包 > 1000)攤平成由大到小的 else-if 鏈;如果數值增長沒規律,就讓每個 else-if 的測試條件不依賴前一個測試的結果Qty > 100 and Qty <= 1000),這時測試條件可按任意順序擺放。
  3. 把 if 巢狀改成 case 語句(p.261):特別是用到整數的測試。並非所有語言都能用,能用時效果很好。
  4. 把深層巢狀裡的程式碼提取成常式(p.261–263):巢狀出現在迴圈中時,把迴圈內部寫成常式;把 if-then-else 分支留在主程式裡以顯示清楚決策分支,各分支裡的程式碼寫成常式。書列出提取後的四個好處:只剩兩層巢狀,結構簡單易懂短的 while 迴圈可以在一屏內閱讀、修改、除錯,不會被幾幕顯示、幾頁列印限制眼界;提取出的常式具有易於修改的一切優點;容易看出可以再改寫成 switch-case,那樣更易讀。

兜底的那一句(p.264):

「一般說來,複雜程式碼表明還沒有完全理解你的程式,應使其更簡單些。深層巢狀提示需把某些部分寫成一個常式或需重新設計複雜的那部分程式。」

步驟 4:兩個通用做法,但其中一個書自己說沒用(17.7.2,p.270)

  1. 做一些動腦筋練習來提高在腦中打底稿的能力——書當場否掉它:大多數程式都很大,而人同時考慮的問題一般都不能超過 5~9 個,因此靠提高腦子容量來降低複雜性能力有限。
  2. 徹底理解你所要解決的問題。這才是書留下的那條路(也呼應 p.264 那句「複雜程式碼表明還沒有完全理解你的程式」)。

步驟 5:設計層級的判準(第 7 章)

拿設計檢查表逐條問(7.5.6,p.109):

  • 設計是智力上可管理的嗎?
  • 設計是低複雜性嗎?
  • 設計是否將常式之間的聯繫保持在最低限度?
  • 設計是簡練的嗎?是不是所有部分都是必要的?
  • 低層次常式是高扇入的嗎?絕大多數常式都是低或中等程度扇出的嗎?

各條在書中的定義:

  • 智力上的可管理性 / 低複雜性(p.108):「智力上的可管理性」對任何系統來說都是重要目標之一,它對整個系統的完整性非常重要,並會影響開發與維護的難易程度;而**「低複雜性實際上是智力上的可管理性一部分」**。
  • 最小的聯繫性(p.108):「指的是按照保持常式之間的聯繫最少的原則來設計,應該利用強內聚、鬆散耦合和資訊隱藏等作為指導原則來設計系統,使其內部的聯繫性盡可能少。最小的聯繫性可以極大地減小綜合、測試和維護階段的工作量。」
  • 扇入扇出(p.108):高扇入表明系統在低層次上充分利用了功能常式;高扇出(大約 7 個以上)說明一個常式控制了許多其他常式,因此可能是很難理解的。Card、Church 和 Agresi(1986):只呼叫 1 個常式的常式有 42% 沒有錯誤,呼叫 2~7 個的有 32% 沒有錯誤,呼叫 7 個以上的只有 12% 沒有錯誤;Card 認為 0~2 個扇出是最優的

分解與抽象為什麼有效,書也講得很直白:

  • 分解的依據(p.96):「自頂向下設計指導原則的依據是:人腦一次只能考慮有限數量的細節。如果你從一個較簡略的常式開始,逐步把它分解成更加詳細的常式,就不必每次考慮過多的細節。」
  • 要分解到什麼程度(p.96):「要持續不斷地分解,直到看起來下一步進行編碼要比再分解要容易為止」——並附一個可以直接拿來自問的判準:「如果連你都對解決方案有些困惑的話,那麼,試想一下,又有誰會理解它呢?
  • 抽象(p.98):「抽象所帶來的主要好處是可以忽略掉無關緊要的細枝末節問題,而專注於重要的特性。」書用房屋比喻:在建造軟體系統時,如果總是在相當於分子的層次上工作,那是不可能建成系統的。
  • 封裝(p.99):「如果抽象對你說『你應該在較高層次上看一個目標(物件)』,而封裝則會說『你只能在這個層次上看一個目標』。」
  • 模組化(p.99):「在理想情況下,模組是高度內聚而又鬆散耦合的。」
  • 資訊隱藏(7.4.2,p.104):「無論什麼問題領域,都應該盡量採用資訊隱藏。使用它沒有任何危險。」——書中唯一一個「無條件推薦」的設計手段。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意

  • 提取成常式只是把決策點搬走,不是消滅它(p.270)。切完檔案數變多、總複雜度不變,換到的是「一次要想的量」變少——如果切完之後你反而要同時開四個檔案才看得懂,那就沒換到東西。
  • 10 個決策點不是絕對上限(p.271):「決策點的最大數目為 10 並不是一個絕對的極限,而僅用這個數目作為一種提醒標誌。」書自己舉的反例是分支很多的 case 語句——這種 case 在事件驅動程式(如 Microsoft Windows、Apple Macintosh 上的許多程式)中用得很多,「在這些程式中一個長的 case 語句可能是降低程式複雜性的最好方法」。不要死套規則。
  • McCabe 不是唯一的度量(p.271):它是用得最多的,特別是考慮控制流問題時,但還有資料使用次數、巢狀層次、行數、輸入輸出點數目等其他方法。
  • 多加的東西就是複雜度——書把它放在「簡練性」底下(p.108):簡練性指的是把系統設計得沒有任何多餘部分;Voltaire 說過,當一本書不能刪掉、而不是不能添補任何內容時,才可以認為它已完成了。理由很實際:冗餘的程式碼你得開發、評審、測試、維護,而且開發新版本時新版本也不得不與這些冗餘程式碼相容。書直接點名:「最有害的觀點是『多加入些又不會有害,怕什麼呢?』」
  • 來源範圍聲明:本卡只採用第一版(1993 中譯本)第 17 章與第 7 章確實寫到的內容。第一版沒有「軟體的首要技術使命是管理複雜度」這種把複雜度立為最高原則的提法(首要技術使命/首要技術/主要技術使命 於全書 0 命中);第一版的立場是 17.8 那句「降低複雜性是編寫高品質的程式碼的關鍵」和第 7 章把「低複雜性」列為設計檢查表中的一項

🔗 相關工具

同書(第一版)其他卡:

  • 工具-建構的先決條件 — 動手前的關卡;第 7 章開頭就說「要確保結構設計先決條件(如 3.4 節所述)已經被滿足了」(p.92)。
  • 工具-軟體評審與檢查 — 決策點是自己算給自己看的,評審是別人幫你看;兩者都在攻同一件事:錯誤與難懂的程式碼要早點被抓出來。
  • 工具-程式碼調校 — 反方向的力:調校常常會讓程式變得更難懂,正好拿本卡的度量當煞車。
  • 工具-專案規模的影響 — 第 7 章的檢查表明說「非正式專案裡有些條款根本不必考慮」,該用多嚴的判準要看規模。

其他來源的對照:

回到來源:程式碼大全

版本與用語

  • 本卡依據《程式碼大全》第一版(Code Complete 1st ed., 1993,簡體中譯)第 5 章 5.6 節「防錯性編程」(p.61–67)+檢查表(p.73)+小結(p.74)寫成,每條都標書內頁碼。書裡沒寫的不寫。
  • 書中的譯法是「防錯性編程」,不是「防禦式編程」。檔名沿用舊名以免斷連結,內文以書的用語為準。
  • 引用一律照原文,只做簡→繁與明顯 OCR 錯字修正;書中的「子程序」在本卡統一寫成常式

🎯 什麼情境該想到我

當你的程式容易被異常輸入/邊界情況弄爆,想讓它更強韌時。

書的類比是防錯性駕駛

「防錯性編程並不意味著要對自己的程式提高警惕,這一想法是在防錯性駕駛的基礎上產生的,在這種駕駛方法中,必須在心中時刻認為其他駕駛員的行為都是不可預測的。這樣,就可以在他們做出某些危險舉動時,確保自己不會因此受傷。」(p.61)

「在防錯性編程中,其中心思想是,即使一個常式被傳入了壞資料,它也不會被傷害,哪怕這個資料是由其他常式錯誤而產生的。更一般地說,其思想核心是承認程式中都會產生問題、都要被改動,一個聰明的程式設計師就以這點為依據開發軟體。」(p.61)


⚠️ 先讀這一條:防錯性編程不是第一順位

書在 5.6 開頭就自己降低了這個技術的優先序:

「最有效的防錯性編碼途徑是一開始就不要引入錯誤。可以採用逐步設計方法、在編碼前先寫好 PDL、進行低層次設計、審查等都可以防止錯誤引入。**因此,應優先考慮它們,而不是防錯性編程。**不過,你可以把防錯性編程與這些技術組合起來使用。」(p.61)

所以順序是:逐步設計 → 先寫 PDL(見 工具-偽代碼編程流程)→ 低層次設計 → 審查(見 工具-軟體評審與檢查)→ 然後才是防錯性編程。


⚙️ 怎麼用(書中 5.6 的九個小節)

1. 使用斷言(5.6.1,p.61–62)

  • 斷言是「一個在假設不正確時會大聲抗議的函數或巨集指令」,用來驗證在程式中作出的假設並排除意外情況(p.61)。
  • 一個斷言函數大致有兩部分:假設為真時的布林運算式,和為假時要印出來的訊息(p.61)。書中的 Pascal 例:
    Assert(Denominator <> 0, 'Denominator is unexpectedly equal to 0.');
  • 用途分期:開發階段用來消除相互矛盾的假設、消除傳入常式的不良數值;維護階段用來表明改動是否影響到程式其他部分(p.61)。
  • 兩條指導方針(p.62):
    1. 有前置處理器就用巨集——開發階段用前置處理器處理斷言,最終程式碼要拿掉斷言時非常容易。
    2. 斷言中要避免使用可執行程式碼——關閉斷言時編譯器可能把它整段拿掉。書的反例與正解:
      ✗ Assert(FileOpen(InputFile) <> NULL, 'Couldn''t open input file');
      ✓ FileStatus := FileOpen(InputFile);
        Assert(FileStatus <> NULL, 'Couldn''t open input file');
      

舊卡上「斷言抓程式設計師的錯、驗證處理使用者的錯,兩者分開」這句已移除——第一版 5.6.1 沒有這個區分,它只講「驗證假設、排除意外情況」與開發/維護階段的用法。

2. 輸入垃圾不一定輸出垃圾(5.6.2,p.62–63)

「一個好的程式從來不會輸出亂七八糟像垃圾似的東西,不管它被輸入的是什麼。一個好程式的特點是**『輸入垃圾,什麼也不產生』,或『輸入垃圾,輸出錯誤訊息』,也可以是『不允許垃圾進入』**。從現在的觀點來看『輸入垃圾,輸出垃圾』,往往是劣質程式。」(p.62)

三件具體的事:

  • 檢查所有外部程式輸入的數值(p.62):從使用者、從檔案讀資料時,確保讀入的資料值在允許範圍之內、數值是可取的、字串短到足以處理,並在程式碼中註釋出輸入資料的允許範圍
  • 檢查全部常式輸入參數值(p.62):與檢查外部資料是同一件事,只是此時由常式代替了檔案或使用者。書給的 C 範例即在 tan() 裡先 Assert(AdjacentLength != 0, ...)
  • 決定如何處理非法參數(p.63):書列出的選項是——回傳一個錯誤代碼、回傳一個中間值、用下一個合法資料代替它並按計畫繼續執行、與上次一樣回傳一個正確答案、使用最接近的合法值、呼叫一個處理錯誤的常式、印出錯誤訊息、或乾脆關閉程式。

    「由於有這樣多的方案可供選擇,所以當在程式的任一個部分處理非法參數時,一定要仔細,確定處理非法參數的通用辦法,是由結構設計決定的,應該在結構設計層次上予以說明。」(p.63)

舊卡「回傳中性值 / 丟例外」已移除或改寫:第一版這份清單裡沒有「丟例外(throw exception)」這個選項,也沒有「中性值」這個詞(書寫的是「回傳一個中間值」)。錯誤處理策略要「全專案一致」這點書有,但書的講法是由結構設計層次規定(見 工具-建構的先決條件)。

3. 例外情況處理(5.6.3,p.63)

  • 應該預先設計好例外處理措施來注意意想不到的情況。
  • 目標是「使意外情況的出現在開發階段變得非常明顯,而在執行階段又是可以修復的」。
  • 書的例子:一個 case 只預計到五種情況——開發階段應利用例外處理產生一個警告提示出現了第六種;產品階段則做得更完善些,例如向錯誤記錄檔寫入訊息
  • 「總之,應該設計出不必費多大周折,就可以從開發階段進入產品階段的程式。」

4. 預計改動(5.6.4,p.63)

  • 改動幾乎是每個程式都不可避免的現象;即使是第一版,也會因為加入沒預計到的功能而改。
  • 「越是可能的改動,越是要容易進行。」
  • 把你在其中預想到的改動域隱含起來,是減少由於改動而對程式帶來影響的最有力武器之一。」

5. 計畫去掉除錯輔助(5.6.5,p.63–65)

除錯輔助=斷言、記憶體檢查報告、印出語句等。自用軟體留著無妨;商用軟體會影響速度與空間,所以要事先計畫怎麼拿掉。書給四種方法:

方法做法
使用版本控制開發版本含全部除錯輔助,產品階段輕鬆去掉不希望出現的部分
使用內建前置處理器#define DEBUG / #if defined(DEBUG) 圍住除錯碼;還可以給 DEBUG 賦值再測值(#if DEBUG > 0#if DEBUG == PRINTER_ERROR#if DEBUG > LEVEL_A)以區分除錯程式碼的不同層次;不喜歡滿場 #if defined() 就自訂 DebugCode(...) 巨集
自己寫一個前置處理器語言沒有前置處理器就自己寫(書說「這項工作是非常容易的」),建立標示除錯碼的約定(例:Pascal 用 /#BEGIN DEBUG//#END DEBUG/),再寫批次檔呼叫它、然後編譯處理過的程式碼——「你不會誤編譯沒有前置處理過的程式碼」
保留使用除錯常式同一個常式留兩個版本:開發版 CheckPointer() 做全面檢查;產品版 CheckPointer() 立即回傳、什麼都不做。對效能影響很小,且比自寫前置處理器快得多,兩個版本都留著供未來使用

6. 儘早引入除錯輔助工具(5.6.6,p.65–66)

「越早引入除錯輔助工具,它們所起的作用也就會越大。一般說來,只有在被某一問題困擾幾次之後,你才會捨得花功夫去編寫除錯輔助工具,但如果你在第一次遇到問題時就這樣做,或者引用一個以前遺留下的除錯輔助工具,那麼它將在整個專案中都會對你有很大幫助。」(p.65–66)

7. 使用「防火牆」包容錯誤帶來的危害(5.6.7,p.66)★

這一節就是舊卡上「Barricade」那條的真正出處,但書不叫 Barricade,叫「防火牆」。

  • 類比:建築物的防火牆把火隔離在一個地方;輪船的分隔式水密艙讓撞上冰山時只有被撞那艙進水,避免「一個洞導致全船進水」。
  • 兩個既有手段可以幫忙建防火牆:
    • 資訊隱蔽——「對另一個常式的內容知道得越少,對它如何操作的假設也就越少,而假設越少,其中一個假設出錯的可能性就會越小。」
    • 鬆散耦合——「兩個常式之間的耦合越鬆散,那麼其中一個常式中的錯誤影響到另外一個常式的機會也越少。」
  • 最好的辦法(=舊卡想講的那件事):

    「在程式中建防火牆的最好辦法是把某些介面標識成『安全』區邊界。對穿越安全區邊界的資料進行合法性檢查,如果是非法的資料,就要作出合理的反應。」(p.66)

  • 另一種說法是手術室技術

    「在資料被允許進入手術室之前,要對其進行消毒處理,手術室中的一切都認為是無毒安全的。」(p.66)
    設計時要回答的關鍵問題(原文照列):「在這個『手術室』中進入些什麼?哪些要被放在它外面?應該把門放在哪兒?應該把哪些常式放在安全區內,哪些放在外面?用什麼對資料進行消毒?」

  • 重要且常被搞反的一句

    「最簡單的辦法是在外部資料進入時對其進行消毒,但是,資料往往需要在幾個層次上進行消毒,因此,有時需要進行多重消毒。」(p.66)

8. 檢查函數回傳值(5.6.8,p.66)

  • 「如果呼叫了一個函數,並且可以忽略函數回傳值(例如,在 C 語言中,甚至不需要知道函數是否回傳一個值),千萬不要忽略這個回傳值。要對這個值進行檢查。
  • 系統函數同樣適用,除非結構設計中明訂不檢查系統呼叫的回傳碼、而是在每次呼叫後檢查錯誤代碼。發現錯誤時,C 的 perror()(或其他語言的等價物)能查出錯誤個數與說明。

9. 在最終軟體中保留多少防錯性編程(5.6.9,p.66–67)★

書點出的矛盾:開發時希望錯誤越引人注意越好;最終產品卻希望它越不顯眼越好。(p.66)五條原則:

  1. 保留查找重要錯誤的程式碼(p.66)——先確定哪些領域可以承受未測到的錯誤、哪些不能。書例:試算表程式的螢幕更新區可以承受(後果只是畫面亂);計算部分不行(會在表格中產生難以察覺的錯誤)。「絕大多數使用者都寧願忍受一個混亂的螢幕而不是錯誤的表格。」
  2. 去掉那些無關緊要錯誤的程式碼(p.66–67)——注意書的定義:「『去掉』並不是指從物理上把這段程式碼刪掉,它指的是版本控制預編譯開關,或其他不編譯那段特定程式碼的技術。」若沒有空間限制,也可以保留它並讓它隱蔽地寫入錯誤記錄檔。
  3. 去掉那些引起程式終止的程式碼(p.67)——開發階段「印出錯誤訊息然後終止」最有用;但最終軟體中「在程式終止前,使用者總是希望有機會將其工作存檔」。含有會使千百萬資料遺失的除錯碼,最終產品中應移除。
  4. 保留那些可以使程式延緩終止的程式碼(p.67)——書的親身例子:文書處理器在記憶體溢位前會亮起「SAVE」警告,讓他先存檔再退出。「我寧願得到一個警告訊息,而不願失去我前面所做的工作。」
  5. 保證留在程式中的錯誤提示訊息是友好的(p.67)——反例是他早年讓使用者在螢幕上看到「你的指標位址有錯,笨蛋!」。通常的辦法是通知使用者存在「內部錯誤」,並給一個可以投訴的電話號碼。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意 / 什麼時候不適用

  • 不要過度防錯(5.6.9 最後一條,p.67):

    「過多的防錯性編程會帶來它自身的問題,如果你對每一種可以察覺的參數傳遞,在每一個可以察覺的地方都進行檢查,那麼程式將變得臃腫而笨拙。更糟的是,附加的用於防錯性編程的程式碼本身並非是完善無缺的,同其他程式碼一樣,你也會在其中發現錯誤,而且,如果你是隨意寫它的,那麼錯誤也會更多。考慮好需要在哪裡預防錯誤,然後再使用防錯性編程。

  • ⛔ 舊卡「在邊界驗證一次,內部就能假設乾淨」這條與第一版原文相牴觸,已移除。 書明說「資料往往需要在幾個層次上進行消毒,因此,有時需要進行多重消毒」(p.66)。真正的節制原則是上面那條「考慮好需要在哪裡預防錯誤」,而不是「只驗一次」。
  • 防錯性編程排在預防手段之後(p.61),別拿它當作省略設計與審查的藉口。
  • 它的功用是縮小損害、不是消滅錯誤(小結,p.74):

    「防錯性編程可以使錯誤更容易被發現和修復,對最終軟體的危害性顯著減小。」

隨手用的檢查表(p.73,5.8.2 檢查表「防錯性編程」段,原文照列)

  • 斷言是否用於驗證假設?
  • 常式對於非法輸入資料進行防護了嗎?
  • 常式是否能很好地進行程式終止?
  • 常式是否能很好地處理修改情況?
  • 是否不用很麻煩地啟用或去掉除錯輔助?
  • 是否資訊隱蔽、鬆散耦合,以及使用「防火牆」資料檢查,以使得它不影響常式之外的程式碼?
  • 常式是否檢查回傳值?
  • 產品程式碼中的防錯性程式碼是否幫助使用者,而不是程式設計師?

🔗 相關工具

  • 工具-建構的先決條件(同書)——書把兩件事推到結構設計層次:處理非法參數的通用辦法「是由結構設計決定的,應該在結構設計層次上予以說明」(p.63),以及結構設計要規定斷言的使用程度與錯誤處理策略。先決條件沒過就談防錯,順序是反的。
  • 工具-偽代碼編程流程(同書)——5.6 開頭列的預防手段之一就是「在編碼前先寫好 PDL」(p.61);反過來,第 4 章收尾檢查也要求確認常式「採用了預防錯誤的設計」(p.41)。兩張卡是同一條防線的上下游。
  • 工具-軟體評審與檢查(同書)——同樣出自 p.61 那句「審查……應優先考慮它們,而不是防錯性編程」;而第 5 章檢查表(p.73)本身就是一份可以拿去做正式檢查的清單。
  • 工具-用例外處理錯誤程式碼整潔之道)——對照用,不是同一套:第一版《程式碼大全》處理非法參數的選項清單裡沒有「丟例外」(p.63),它給的是錯誤碼/中間值/最接近的合法值/呼叫錯誤處理常式/終止等。要拿例外當策略時,請知道那是另一本書的主張。
  • 工具-MQ消費端防禦三原則——實務對應:訊息佇列消費端就是典型的「安全區邊界」,在邊界做合法性檢查與合理反應(p.66)。
  • 回到來源:程式碼大全(第一版,第 5 章 5.6 節「防錯性編程」,p.61–67)

🔍 找出錯誤

🎯 什麼情境該想到我

當你以為「測完就沒事了」的時候。 這本書用一整排實證數據說明:測試單獨使用的檢錯能力遠比想像中低,而正式的**檢查(inspection)**才是抓錯主力。

單一的軟體測試方法只能取得有限的效果——單元測試的檢錯比僅為 25%,功能測試為 35%,而集成測試為 45%,相比之下,設計和程式碼檢查的檢錯比平均為 55%、60%。(p.384)

錯誤發現百分比(表 23-1,摘自 Jones 1986,p.379)

步驟最低比中等比最高比
對設計文件的人工檢查15%35%70%
非正式組內設計檢查30%40%60%
正式設計檢查35%55%75%
正式程式碼檢查30%60%70%
模型或原型35%65%80%
人工程式碼檢查20%40%60%
單元測試(單個子程式)10%25%50%
功能測試(相關於程式)20%35%55%
系統測試(整個系統)25%45%60%
段測試(動態資料)35%50%65%
集成測試(綜合使用)93%99%99%

書中對此表的兩點結論(p.379–380):

  1. 任何單一方法的中等比都不超過 65%——單靠一招是不夠的。
  2. 要拿到高檢錯比就必須綜合幾種方法。Jones 指出單元測試、功能測試、系統測試綜合應用的集成檢錯率不會低於 60%,這對商業軟體是合適的(p.380)。

實際案例的數字(p.385,除註明外)

案例數字頁碼
某次軟體維護:引入程式碼檢查,聯機(線上)維護修改是錯誤的比例55%p.385
同上,引入程式碼檢查2%p.385
應用程式碼檢查法一次就可改正的錯誤比例95%p.385
未用程式碼檢查法,一次能糾正的錯誤比例20% 以下p.385
同一開發組 11 個程式:未用評審的 5 個,每百行程式碼錯誤數4.5 個p.385
同上,經過評審的 6 個,每百行程式碼錯誤數0.82 個(減少 80% 以上)p.385
Aetna 保險公司:可通過檢查發現的錯誤比例82%p.385
Aetna 保險公司:檢查可將開發資源減少25%p.385
IBM 500,000 行 Orbit 專案(用了 11 種檢查方法):所產生的錯誤僅為正常情況的1%p.385
AT&T 一個超過 200 人的機構:採用評審檢查後生產率提高14%p.385
同上,錯誤減少90%p.385
噴射推進實驗室估計:早期發現和定位錯誤,每次檢查可節省25,000 美元p.385
設計與程式碼檢查聯合使用可去除產品中的錯誤60% 到 90%p.386
檢查可提高生產率約20%p.386
使用設計和編碼檢查的專案,檢查佔專案總時間15%p.386

檢查 vs. 除錯的成本(第 23 章)

比較數字頁碼
微軟應用部:程式碼檢查一步法發現並改正一個錯誤3 小時內p.380
微軟應用部:除錯二步法發現並改正一個錯誤12 小時內p.380
Collofello 與 Woodfied,700,000 行、逾 400 人的專案:程式碼檢查與除錯的收效比1.38 : 0.17p.380
NASA 軟體工程實驗室:閱讀程式碼比除錯每小時多發現的錯誤80% 多p.380
NASA 軟體工程實驗室:程式碼閱讀每小時發現錯誤數 vs. 除錯(Card 1987)3.3 個 vs. 1.8 個p.391
NASA 軟體工程實驗室:只有兩位程式碼閱讀者,通過程式碼閱讀發現的錯誤29%p.379

書的關鍵論證(p.380):有些方法(如人工檢查)一步就完成「發現+改正」,測試則是「發現」之後還要另外分析與改正的二步方法。一步法比二步法更划算。


⚙️ 怎麼用

書把「評審」當成總稱(檢查、普查、程式碼閱讀等別人查看你工作的方法,p.384),其中檢查(inspection)是最有實證支持的那一種(Michael Fagan 發明,在 IBM 用了幾年後由 Fagan 出版發行,p.386)。以下是書中檢查的可執行流程。

一、先確認你做的是「檢查」而不是隨便看看(p.386)

檢查與走馬看花式的參觀不同,必須具備:

  • 檢查表把檢查員的注意力集中在過去所遇到的問題領域
  • 側重於錯誤檢查而不是糾錯
  • 評審者在檢查之前先舉行預備會議,最後得出的問題經過共同討論
  • 所有參加者被分配不同的任務
  • 檢查協調者不是被檢查產品一方的人
  • 協調者在協調檢查方面受過訓練
  • 每次檢查的數據都被收集,回饋給今後的檢查
  • 一般管理人員不參加檢查會議(技術主管則可能參加)

二、分派角色(p.386–387)

角色職責
協調者控制檢查進度(既有速度又能最大限度發現錯誤);分配受檢的設計/程式碼/檢查表、確立會議室、報告檢查結果、落實會議所確定的內容。須有技術能力但不必是該設計或程式碼的專家。
專案主持者(設計者/程式碼撰寫人)在檢查中扮演相對次要的角色。若設計或程式碼不清晰,主持者被告知修改使其更清楚;否則要解釋不清晰處,甚至解釋為何看起來有錯的部分實際可接受。若檢查者不熟悉專案,由他做簡介。
檢查者任何對設計或程式碼有直接興趣、但不是本專案主持者的人(可包含測試者或高水平的結構人員)。任務是發現缺陷
記錄員記錄所發現的錯誤與會議情況。可由協調者或另外的人兼任,但主持者與檢查者不得兼任
管理者有權知道檢查結果,檢查報告應讓管理者知道;但不參加檢查會議——管理者出現會把重心從技術轉移到政治上。

規模(p.387):

  • 一次檢查至少三個參加者:協調者、主持者、檢查者,且三個角色不能各自兼任
  • 一般把檢查組控制在 6 人左右,再多就難以管理。
  • 噴射推進實驗室的研究:三人以上的檢查組並不能提高所發現的錯誤總數

三、跑七個階段(p.387–388)

  1. 計劃——主持者把設計或程式碼交給協調者;協調者決定誰參與、何時何地開會,並把設計、程式碼與檢查表分發給每位檢查員。
  2. 總覽(僅在檢查員不熟悉專案時)——主持者花一小時左右介紹產生設計或程式碼的環境。書明講這件事不容易,因為它容易給需檢查的東西造成不清晰的假象;設計或程式碼應能自己站得住腳,總覽不應提及它。
  3. 準備——每位檢查者花 90 分鐘熟悉設計或程式碼,使用檢查表指導檢查工作。
    • 高階語言的應用程式程式碼:每小時可先閱讀 700 行
    • 高階語言的系統程式程式碼:每小時只能閱讀 125 行
    • 最有效的速度差異很大,要記錄自己團隊的速度
  4. 檢查會議——協調者請人(通常是主持者)解釋設計或程式碼的閱讀,所有邏輯都要解釋,包括每個邏輯結構的分支。發現錯誤就記錄下來並標明重要性,確認是錯誤後就停止討論,繼續往下走
    • 最佳檢查速度:系統碼每小時 90 行應用程式碼每小時可提高到 500 行。太慢注意力會鬆懈且效率低,太快會漏掉應發現的錯誤。
    • 會議中不必討論解答,應著重於確定錯誤。有些檢查組甚至不允許爭論「這算不算真錯誤」——因為如果錯誤的是非會被弄混淆,相應的設計、程式碼與文件本來就需要澄清。
    • 會議不超過兩小時。IBM 與其它公司的經驗:檢查者無法一次全神貫注超過 2 小時。同一天安排多次檢查也不明智。
  5. 檢查報告——會議後協調者編制報告,列出每個錯誤的類型與嚴重性。若能持續收集「所花時間」與「所發現錯誤數」,就能用硬數據判定檢查員的工作效率。
  6. 再工作——協調者把錯誤交給某人(通常是主持者)逐一改正。
  7. 執行——協調者負責檢查再工作的執行。若超過 5% 的設計或程式碼需再加工,整個檢查過程應重新進行;低於 5% 則協調者可要求再檢查或親自證實。

⚠️ 書的硬性要求:檢查過程的所有部分都是必需的。「已經發現,去掉或合併其中任意幾個部分都將耗去更多的代碼。如果你想改變檢查過程而沒有一種可以度量改變的方法,那麼你就別這樣做。」(p.388)

四、養一份自己的檢查表(p.388)

  • 觀察哪些類型的錯誤較常發生,建一份表把注意力引到那裡。
  • 時間久了,發現新錯誤就加進去,不再出現的錯誤就從表中拿掉
  • 把檢查表限制在一頁之內——太長的檢查表在所要求的細節水平上很難使用。

書中「有效檢查法」自檢清單(p.389):檢查表是否引向過去常發生錯誤的地方?是否側重缺陷檢查而非糾錯?會前每位檢查員都準備夠了嗎?每位參與者是否扮演不同角色?會議是否限制在 2 小時內?協調者是否受過特殊訓練?是否收集了錯誤類型數據與準備/檢查率?指定的條款是否都落實?管理員是否明白為什麼他不參加?

五、選不了檢查時的替代方案(24.3 節,p.389–391)

方法書怎麼說頁碼
普查(walkthrough)定義寬鬆,兩人以上討論設計或程式碼即可;通常花 30–60 分鐘;由主持者採用;侧重發現錯誤而非糾正;管理人員不參加。能發現 30%–70% 的錯誤(Myers 1979、Boehm 1987、Yourdon 1989)。p.389–390
程式碼閱讀開會前把 1000–10000 行(典型 4000 行)源碼分發出去;至少 2 人各自閱讀(速度約每天 1000 行);讀完由開發者主持 1–2 小時討論會,會議不是必需的。特點是側重個人閱讀而非會議討論,評審員不在一起時相當有價值p.391
軟體演示向用戶展示軟體品質良好,屬於管理評審而非技術評審p.391

檢查 vs. 普查的差別(p.390)——檢查全是「是」,普查全是「否」:正規協調者培訓、參與者分工明確、有檢查表告訴你「怎樣發現錯誤」、集中評審量尋找最常見類型的錯誤、正規執行以減少不正確的定位、對結果的分析導致效率提高、對「導致發現錯誤的數據」的分析反過來也提高效率。此外普查由作者主持(檢查由協調者主持),且普查因回饋較弱,對減少後期錯誤「影響不大」。


🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 檢查不是為了評估人。 「檢查結果不應該用來作為對性能的評估。不要殺死正在下金蛋的天鵝。」性能評估應建立在最終產品之上,而不是未完成的工作之下(p.387)。
  • 不准人身攻擊。 檢查組不能對主持者說「有一些人是笨蛋應讓其捲鋪蓋滾蛋」;像「任何知道 Pascal 語言的人都知道從 Num 迴圈到 0 是更為有效」這類評論根本不合適,協調者應把這點明白無誤地表達出來(p.388)。
  • 主持者對錯誤有最終處置權。 主持者應接受每個被指出的錯誤並繼續下去——接受評論不代表認同它正確;檢查之後主持者可獨立考慮每個問題並決定是否有效。檢查者應尊重這個權力(p.388)。
  • 評審不能取代測試,是互補。 「評審往往比測試要能發現更多種不同的錯誤,意味著你應使用評審或除錯方法以確保軟體品質」(p.392);且「即使除錯相當有效,評審對一個程式的集成性質量保證是必需的」(p.385)。
  • 普查用不好會虧錢。 普查的最低效率為 30%,「這並無多大價值」。至少一個組織(Boeing Computer Services)發現對程式碼的掃視檢查「異常不合算」;Boeing 還發現說服人員自始至終應用普查很困難,壓力增大時幾乎不可能(Glass 1982,p.390)。
  • 成本評估的前提要注意。 1978 年 Myers 研究之前,人工檢查的代價是測試的兩倍——但書指出該研究對象缺乏檢查經驗;人有了經驗後檢查才更有效。用某系統三次交付(發現率 15% → 41% → 61%)修正後,可得出「對每個錯誤來說檢查的代價僅為除錯的一半而不是兩倍」的結論(p.380)。
  • 某些專案品質保證確實更貴。 「如果你在為航天飛機或生命保障系統編寫程式碼,可靠性需求會使本專案花費更多」(p.382)。
  • 關於表格數字的一點小心。 第 24 章行文說「集成測試為 45%」(p.384),而第 23 章表 23-1(p.379)中 45% 對應的是「系統測試(整個系統)」的中等比,表中「集成測試」那列是 93/99/99(綜合使用多種方法)。引用時請照原頁碼標註,不要混用。

🔗 相關工具

  • 工具-建構的先決條件(同書)——錯誤越早抓越便宜。本卡是同一原則的下游證據:「錯誤進入軟體的時間越早,它就越深藏於軟體的其它部分中,也就越不易將其移去」(p.381);「你發現錯誤越早,其所引起的損失也越小」(p.383)。
  • 工具-管理複雜度(同書)——檢查會議要求「所有的邏輯都要解釋,包括每個邏輯結構的分支」(p.387);解釋不清的程式碼本身就是被檢查出來的缺陷。
  • 工具-系統化除錯程式設計實務)——分工不同:檢查是預防(在錯誤進入產品前攔截),除錯是事後(錯誤已經發生後追查)。書把兩者當成互補而非替代:人工檢查與電腦除錯各自擅長不同類型的錯誤,「成對地使用錯誤檢查方法要比單獨使用好」(p.380)。
  • 回連 程式碼大全(第 23 章「軟體品質概述」+第 24 章「評審」,p.375–393)

⚡ 效能

🎯 什麼情境該想到我

當你「覺得」程式跑得慢、忍不住想動手把某段改快一點時。特別是當你腦中冒出「這樣寫應該比較快」——那個「應該」就是這張卡要攔下來的東西。

一種操作也許比另一種操作更快或更省空間——錯誤!當你談論效能時不存在「也許」這個概念。你的修改是對程式有益還是有害,必須通過對效能測量來判斷。(p.449)

⚙️ 怎麼用

步驟 0:先確認「調校」是不是對的工具

調校排在改善效能手段的最後一位。書列出的層次依序是:程式設計 → 模組與子程式設計 → 與作業系統的互動 → 程式碼編譯 → 硬體 → 程式碼調校(p.446–447)。

為什麼要用程式碼調校?它不是最有效的提高程式效能的辦法。程式設計、資料結構選擇和演算法選擇通常能產生更好的改進效果,它也不是最簡單的改進效能的方法,買一個新的硬體,或更好的編譯器更加簡單。(p.448)

而且編譯器常常贏過你:

用一個好的最佳化編譯器,你的程式碼速度可提高 30% 或更多,在下一章講的技術,速度只能提高 20%,為什麼不編寫一個清楚的程式碼,讓編譯器去最佳化它呢?(p.452)

步驟 1–5:書的正式流程(p.457)

  1. 用高度模組化的設計開發軟體,讓它易於理解、修改。
  2. 如果效能很差,測量系統,找出頻繁執行的位置。
  3. 判斷弱點是不是設計、資料結構、演算法造成的;判斷程式碼調校是否合適。若不合適,回到步驟 1(那是結構問題,局部調校救不了)。
  4. 只調校步驟 3 識別出的薄弱環節,測量每一個改進;如果它不能提高系統效能就放棄重來。
  5. 回到步驟 2 重複。

判準補充

  • 什麼時候開始調校:先做出高品質、正確、模組化易改的設計;程式完整正確後再檢查效能;「如果程式設計者能使它快速、簡單,就先不要最佳化,等到你知道你需要對它最佳化時」(p.453)。
  • 往哪裡找(Pareto 80/20,p.450):Boehm 報告 20% 的程式段消耗 80% 的執行時間;Knuth 1971 年對 FORTRAN 程式的實證研究發現 不到 4% 的程式常佔用超過 50% 的執行時間。p.484 另給了「約 5% 的程式碼通常消耗整個程式 50% 以上執行時間」。
  • 量測要夠精確(p.452):「用數一隻象、二隻象、三隻象的方法測定程式時間是不夠精確的」。用表分析工具,或自己用系統時鐘記錄。在多使用者/多工系統上,要用分配給你程式的 CPU 時鐘週期數量測,不要用時間——否則系統把你的程式換出去時,別的程式的時間會算到你頭上。
  • 改完要再量一次:確定究竟改進了多少(p.450–451)。
  • 什麼時候停:不是找到第一個改進就停。「第一次最佳化後不要停止,即使第一次產生了顯著效果,第二次最佳化會更好」(p.476、p.486)。單一手法很難拿到 10 倍,但累積會很可怕——作者的 DES 加密程式做了約 30 輪調校,從 21:40 降到 0:22(98%),其中沒有任何一輪超過 5% 也照樣有意義(p.453)。真正的停止條件是事先為子系統定好的空間與速度目標達成了(p.447)。

手法清單(第 29 章,挑至今仍成立的)

迴圈

  • 反切換:迴圈內判斷條件不變時,把 if 提到迴圈外(C 省 21%、Basic 19%,p.459–460)。
  • 合併(fusion):兩個跑同一元素集合的迴圈併成一個(2–4%,p.461)。
  • 展開:一次處理多個元素(21–28%,再展開再省 10%,p.461–462)。
  • 最小化迴圈內工作量:複雜指標運算式先算好存變數,迴圈裡用變數(C 13%,p.463)——這條通常同時提高可讀性。
  • 檢索迴圈用標記值(sentinel):把要找的值塞在陣列尾端,三個判斷併成一個(整數陣列 28–91%,p.463–464)。
  • 最忙的迴圈放內層(p.464)。
  • 降低運算強度:用加法代替乘法(21–26%,p.465)。

邏輯

  • 知道答案就停止判斷:短路求值、找到就 break(27%/9%,p.466–467)。
  • 按頻率排 case / if-then-else 的順序:最常出現的放最前面(33–37%,p.467–468)。
  • 用查表法代替複雜邏輯判斷鏈(31–60%,且 C 的例子中程式碼從 88 位元組降到 43 位元組,含表本身;規則變了也更好維護,p.468–469)。
  • 偷懶改進法:需要時才算,算完存起來(p.469)。

資料與運算式

  • 盡量用整數不用浮點;減少陣列維度;減少陣列存取次數;運用輔助索引(長度索引、單向串列的索引表);把常用值放進快取(p.470–474)。
  • 利用數學等價關係sqrt(x) < sqrt(y) 換成 x < y(80%/60%,p.474)。
  • 編譯期先算好:把 log(2) 換成命名常數(25%,p.475–476)。
  • 注意系統子程式的精度過剩:整數版 log2 用一串整數比較取代浮點 log()99.5%、200:1(p.477)。書的原話是這些函式「設計精度可以把太空人送到目標上離目標只差正負 2 英尺的範圍內,如果你不需要這麼高的精度,你也不需要花費這麼長時間計算它」(p.476)。
  • 預先計算結果消除公共子運算式(p.478–483)。

子程式

  • 「在程式碼調校中,最強有力的手段之一是好的程式分解。小的、明確的程式可以節省空間……它們使程式更容易最佳化」(p.482)。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

書明確標「錯誤!」的四個反模式

  1. 「減少高階語言中語句的行數可提高機器碼的執行速度或減小空間——錯誤!」(p.449)
    五行直接賦值 vs. 一行 for i=1 to 5 do a[i]=i,實測直接賦值贏:Pascal 0.660→0.110(省 83%6:1)、Basic 0.379→0.051(省 87%7:1)。行數少 ≠ 跑得快。

  2. 「一種操作也許比另一種操作更快或更省空間——錯誤!」(p.449)
    「在使用一種編譯器的一台機器上所能運行的,在使用另一種編譯器的另一台機器上可能就是錯誤的。」

  3. 「你應該處處最佳化程式——錯誤!」(p.449–450)

    這是一種只見樹木不見森林的看法,在這種情況下,程式設計師往往因為過於注重局部最佳化而忽視了更重要的整體最佳化。

    書給的三個具體後果:(a) 在程式完整工作前你幾乎不知道哪個部分最弱,時間花在不需要最佳化的地方;(b) 少數猜對的人又過分注重已知弱點而忽視其他,最終反而降低系統效能;(c) 正確性、模組化、資訊隱蔽、可讀性全被降為次要目標。

    即使效能改進容易,一個效能只能影響 5% 的程式碼,你想要運行中 5% 的效能改進,還是想要 10% 的可讀性!(p.450)

  4. 「一個快速的程式和一個正確的程式一樣重要——錯誤!」(p.450)
    Weinberg 講的故事結尾,新手對著「我的程式每個輸入只要一秒」的老手說:

    你們說的對,像你們的程式不能工作,如果要求我的程式不工作,我也能使它快速運行還不需要花一分錢。

兩種「幫倒忙」

  • 白做工:「如果你更換或改進了編譯器,新的編譯器可能會自動按照手工調整程式碼的方法最佳化程式碼,這時你的工作可能就白做了。」(p.449)
  • 反效果:「更嚴重的是你的程式碼調校破壞編譯器的最佳化」(p.449)。實例在 p.471:把二維陣列改成一維,Pascal 省 32%、C 省 25%,C++ 卻是 -79%——「可能是你的程式碼調校妨礙了編譯器的自動最佳化,在這個例子中編譯器最佳化似乎比手工更有效。」

憑經驗猜,猜錯的機率很高

作者把矩陣加總的雙層迴圈改寫成指標版,估算能省掉 100 次乘法。實測結果:

沒有一點改進。對於 10×10 矩陣、3×3 矩陣、25×25 矩陣都沒改進,編譯器的最佳化器已經將第一個程式碼最佳化得足夠好了……我得到的教訓是:沒有測量效能保證的最佳化,其結果常常是使你的程式碼難讀。(p.451–452)

經驗也不能對最佳化有很大幫助,一個人的經驗可能是從一種老的機型、語言或編譯器中得來。當這些東西中的一種改變時,所有預測就都作廢。(p.451)

同一手法在不同環境結果相反(書自己給的負值)

手法好的情形壞的情形頁碼
最忙迴圈放內層Pascal 4%、Ada 5%Fortran −81%p.464
二維陣列改一維Pascal 32%、C 25%C++ −79%;索引改長整數後 −9% ~ −124%p.470–471
快取計算結果Pascal 14%、C 3%Basic −44%p.474
檢索迴圈用標記值整數陣列 28–91%單精度浮點陣列 C++/Pascal −3%p.464
消除公共子運算式C 用指標版 13%C −3%、Ada −11%(用值代換那版)p.482
常數型別一致C 74%C++ 0%、Fortran 0%p.478

盲目聽從任何一種程式碼調校建議都是危險的,在你沒有在自己特定的環境下嘗試過建議前,你不能做出任何肯定。(p.471)

代價是可讀性與維護性——這是本章的底色

  • 第 28 章開頭就把 1970 年代的教訓寫死:程式設計師「只注意效能,嚴重地破壞了系統的可讀性和維護性」(p.446)。
  • 作者那個 98% 改進的 DES 例子,代價是「最後的程式碼也是我所編程式碼中最難讀、最不可維護的……通常,程式碼調校和程式碼品質間的關係都是這樣」(p.453)。
  • 效能不等於品質:「你的程式碼運行速度並不代表其他效能品質,如果你提高程式碼運行速度而犧牲其他效能,這不但不能改進功能,還會損害功能。」(p.447)
  • 各手法自帶的坑:反切換讓兩個迴圈必須平行維護(p.460);迴圈合併可能讓兩邊索引不再匹配、順序被破壞(p.461);迴圈展開容易出邊界錯誤(p.462);標記值必須小心選值並記得還原原值(p.464);一維化陣列的 NumRows*NumCols 可能索引溢位(p.471)。

局部調校救不了結構問題

在某些工程中,有些最佳化法不能滿足效能要求,你將不得不更大地改動已編成的程式碼……這種情況中問題不是出在程式碼品質不好,而是軟體結構不能滿足要求。(p.450)

調校本身是一次「修改程式」,套用第 30 章的紀律

第 30 章(p.486–499)談的其實是軟體演化/修改(中譯標題作「軟體優化」易誤導),不是效能最佳化。它的中心規則是「提高程式的內在品質」,檢查表裡有一條直接適用於調校:「軟體是否作了重測試,確保修改未使效能變壞?」(p.498–499)另外書引 Weinberg:「十個最昂貴的程式設計錯誤都涉及修改現存的程式,最貴的幾個每個都值上千萬美元,儘管只需改動一行」(p.488)——調校改的是已經正確的程式碼,回歸測試不能省。

⚠️ 已過時(1993 年的機器與編譯器假設,僅供理解書中例子)

  • 表 28-1「常用操作的耗費」(p.456–457)的相對值以浮點在軟體中模擬、而非硬體為前提(C 的浮點賦值 85、浮點指數 2000)。今天的相對成本完全不同。但「自己在自己環境做一張這樣的表」這個做法仍成立——書自己就說「在你的環境測試一下你感興趣的操作」。
  • 格式化列印額外佔 1.6K–3.3K 位元組、第一次浮點運算就拉進整個約 15K 的浮點函式庫(p.454–455)——那是當年 PC 的空間問題。
  • 分頁法例子用 2K 頁、每次缺頁中斷千分之一秒(p.455)——具體數字過時;「存取順序要配合資料在記憶體中的佈局」的道理仍在。
  • 用組合語言重寫關鍵子程式(p.483–485,Pascal 省 65%)——今天只在極少數情境成立。
  • 特定語言/編譯器的個別行為(Basic 變數預設單精度浮點、Pascal 的串長度位元組、編譯器與連結器的覆蓋支援)——全部過時。但正因為結果隨環境劇烈變動,這些過時例子反而強化了本卡的核心主張:不量測就不算數。

🔗 相關工具

  • 工具-管理複雜度 —— 同書的最高準則,也是這張卡的煞車:調校常常是拿可讀性換效能,動手前先問「這讓下一個人要理解的東西變多還是變少」
  • 工具-前端效能優化規則 —— 不同層級:那張管網頁交付層(HTTP 往返、資源、CDN),這張管程式碼層(迴圈、運算式、資料結構)
  • 工具-快取與資源優化 —— 同為網頁交付層高性能網站建設指南);本書 p.473–474 也講快取,但指的是程式內部把常用計算結果存起來,不是瀏覽器/CDN 快取
  • 程式碼大全 —— 回到書

📏 規模

🎯 什麼情境該想到我

當你發現「以前這樣做都好好的,怎麼這次專案一變大就整個垮掉」——工作量、錯誤、開會時間全都比你想的多很多,而且多的比例還各不相同的時候。

核心觀念:規模不是線性放大的。程式長度變 10 倍,工作量、建構、整合測試、錯誤各自以完全不同的倍率成長,所以小專案的做法直接搬到大專案會失敗。

「雖然最終軟體的大小是所預期的 10 倍,並不意味著也需花費 10 倍的工作量,而可能是 20 倍的工作量。而且,20 倍的工作量並不意味著 20 倍的建構,也可能是 12 倍的建構和 40 倍的系統整合和測試。你也將可能獲得不僅是 10 倍的錯誤,而是 15 倍的錯誤。」(p.351)


⚙️ 怎麼用

第 0 步:先定位你在哪一格

書給了兩張分布表。先確認你的專案「典型不典型」,再決定要不要沿用直覺。

用人數定位(p.351)

專案組人數專案所占百分比
1~350%
4~833%
8~126%
12~205%
20~503%
50+2%

用程式長度定位(p.352)——「大的專案往往使用更多的程式設計師」

程式長度(以程式碼長度計算)程式設計師所占百分比
2k5~10%
2k~16k5~10%
16k~64k10~20%
64k~512k30~40%
512k+30~40%

判準:一半的專案是 1–3 人。如果你的經驗都來自這一格,你對「大專案」的所有直覺都要重新校正。

第 1 步:查倍率——別用同一個係數放大所有活動

規模變化建構系統整合與測試計劃錯誤總工作量
大 2 倍(p.353)2 : 14 : 1多於 2 倍(p.356)
大 10 倍(p.351)12 倍40 倍15 倍可能 20 倍
大 10 倍(p.353)12 倍40 倍(整合測試量)100 倍

「建構之所以隨著專案的增大而變弱,是因為建構的各種活動——詳細設計、除錯、編碼單元測試——是線性增長,而其它各種活動則是按乘冪的關係增長的。」(p.353)

會隨專案增大而工作量增大的其它活動(p.353–354):計劃、管理、交流、需求開發、系統功能設計、介面設計和描述、總體結構、整合、錯誤消除、系統測試、文件生成。

判準:你的估算是不是只算了建構? 如果是,你就漏掉了成長最快的那一整排。

第 2 步:查建構佔比——決定你的估算會偏掉多少

專案規模建構佔開發活動時間原文(p.353)
小專案80%「建構是最為突出的活動」
中專案「建構仍然是主導性的活動,但是所佔比例下降了 50%」
很大的專案「結構、整合、系統測試和建構所占時間比例大致相等」

對應的估算誤差(p.356),這是最可據以決策的一組數字:

你的做法結果
用自己寫 2K 行的經驗估一個 2K 行的程式所估時間僅為實際所需時間的 80%
沒有考慮各種非建構活動所費時間實際所花時間比估計的多 25%
用自己 2K 的建構經驗評估 32K 程式所估時間僅為實際需要的 50%
用過去的編程經驗評估「建構一個系統產品」可能低估近 10 倍
不理解產品其它工作的重要性估計錯誤數可增加 3 倍或更多

第 3 步:查「你做的到底是什麼東西」——不是只有行數在決定規模

「並不只是程式碼行數和參加人數對專案的大小有影響。一個更微妙的影響是軟體的品質和複雜性。」(p.356)

產物型態定義(p.356)代價
程式由使用者自行開發的單一程式1 倍
產品為了讓其它人而不僅是使用者自己使用;交付前深入除錯、被文檔化、可能被其它人維護程式開發代價的 3 倍
系統要求一群程式設計師相互協作開發;需開發各不同部分之間的介面以便有機整合單一程式的 3 倍
系統產品加裝產品外殼(使用者介面),並將系統各部分整合起來單一程式的 9 倍

判準:你答應的是「一個程式」還是「一個系統產品」? 兩者差 9 倍,而這是「導致估計錯誤最為常見的原因」(p.356)。

第 4 步:查該用多少正規性——規模決定形式化程度

書用「專案正規性明細表」(表 21-1,p.354–355):對 12 個因素各評 1–5 分,總分落在 12–60 之間。

12 個因素:初始要求、通用性、操作跨度、範圍和對象的修改、設備複雜性、人事安排、開發代價、關鍵性、對程式修改的平均反應時間、對資料輸入的平均反應時間、程式語言、軟體開發中的合作性。

其中兩個因素直接錨定規模:

評分12345
人事安排1-23-55-1010-1818 以上
開發代價3-15k15-70k70-200k200-500k大於 500k

「得 12 分意味著對專案的要求是輕微的,且對其正規性要求很少。得 60 分意味著專案要求很高,且對結構的規定也很多。……在軟體開發中,專案越正規,你就越需付出更多的工作量。」(p.355)

文件建議(表 21-2,p.355)——查到分數就查到該產出的文件層級:

正規評分建議文件
12-15使用者指南和小程式文件
15-26低級文件以及操作使用手冊、維修手冊、測試計劃、管理計劃、結構配置計劃
24-38低級文件加功能描述、品質確保計劃
36-50低級文件以及系統和子系統描述、測試分析報表
48-60低級文件以及程式描述

(區間重疊為原書表格所載,照錄。)

寫文件的理由不是為了那份文件本身:

「你編寫配置管理計劃並不是為了運行有關程式,而只是強迫自己能向其它人解釋。……如果你自己感到要編寫一般的文件,這就有點不對頭了。」(p.355)

第 5 步:算溝通成本——這是規模稅裡最容易被忽略的一項

「如果只有你一人開發專案,對專案成功或失敗影響最大的正是你自己。如果你所在專案開發組有 25 人,你也可能仍發揮最大作用,但更可能是大家各有一份功勞,整個集體對專案的成功或失敗有更大的影響。」(p.352)

交流途徑的成長(p.352):

程式設計師人數交流途徑
21
33
46
510
1045
50+至少 1200

「交流途徑越多,你放在交流上的時間越多,也就越容易出現錯誤。大的專案要求採用一定的方法以簡化交流方法,以對其作一定程度的限制。」(p.352)

對策(p.353):規範化交流方法——「不是讓 50 個人按各種可能方式相互間進行交流,而是讓 50 個人都閱讀和編寫文件,有些是文字的,而有些則是圖形的,有些是列印在紙上的,或是表格形式的。」

方法(method)的使用也隨規模改變(p.354):「對小系統,方法往往是偶然的和本能的。對大專案,方法往往是精確和仔細計劃的。」

不論專案大小都有價值的做法(p.354):結構化編碼、其它程式設計師對邏輯和程式碼的檢查、聯機開發系統、高階語言的使用。

第 6 步:預期錯誤密度與生產率會往哪邊掉

錯誤來源的組成隨規模變(p.356)

專案規模建構錯誤佔整個錯誤
一些小專案75%(「方法對程式碼品質影響不大,對程式品質影響最大的是個人編寫程式的技巧」)
大專案50%(「較大專案需要更多的分析和設計,所以此類活動中發生錯誤的機會相應也大一些」)
一些很大的專案(500,000 行)75%以上

錯誤密度(表 21-3,p.357,出自 Jones 1977)

專案大小(程式碼行數)每 1000 行程式碼所發生錯誤數
小於 2k0-25
2k-16k0-40
16k-64k0.5-50
64k-512k2-70
512k 或更多 ※4-100

※ 原書此列印作「52k 或更多」,依相鄰級距推斷應為 512k。
結論(p.357):「大專案出現錯誤數是小專案的 4 倍多」;「對超過一定大小的專案,不能編寫出沒有錯誤的程式碼,錯誤總可以潛入,而不管採用什麼方法預防錯誤。」

生產率(表 21-4,p.358,出自 Jones 1977)

專案大小(程式碼行數)每月程式碼行數
小於 2k333-2000
2k-16k200-1250
16k-64k125-1000
64k-512k67-500
512k 或更多36-250

結論(p.358):「最小專案的生產率是最大專案的 10 倍。即使從某一中等專案轉移到另一中等專案——如從千行程式碼的專案轉到 9 千行程式碼的專案——生產率也可降低一半。」

另外(p.357):對於小專案(200 行或更小),對生產率影響最大的是單個程式設計師的技巧;隨著專案增大,開發組人數和組織對生產率影響迅速增大。Boehm 和 Gray 曾說過,較少人數的開發組的效率比人數較多的高 39%


🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 不要把不同段落的倍率混用。 p.351 是「軟體最終長度是預期的 10 倍」這個情境(工作量 20 倍/建構 12 倍/系統整合與測試 40 倍/錯誤 15 倍);p.353 是「一個專案是另一個的 10 倍大」這個比較(建構 12 倍/計劃 100 倍/整合測試 40 倍)。前者談的是同一專案膨脹,後者談的是兩個專案相比。
  • 表 21-3、21-4 的數字有它的前提。 書自己就說了:「以上數據只是來自一些特定專案,你實際所出現的錯誤數可能和以上數據略有出入」(p.357);生產率「實際上是由人員的素質、程式語言、方法、產品複雜性、編程環境、工具支持和其它許多因素所決定的,所以可能表 21-4 的數據並不能直接用於你的編程環境,你可視具體情況而定」(p.357)。當方向感用,不要當承諾。
  • 反向也成立:大專案的做法搬到小專案同樣是錯的。 「如果你已習慣開發中等專案,你可以利用你的經驗進行一次小專案開發」(p.351)——正規性要跟著往下調,不是往上抄。「正規的方法並不令人高興。如果誤用了,往往會引起麻煩」(p.354)。
  • 表 21-1、21-2 的正規性尺度帶有軍方採購色彩(因素含「操作跨度:廣泛地適用於各軍事部門」、「關鍵性:國家防務」),書明講軍方是最大用戶與最大研究贊助者之一(p.354)。當作一種「規模→形式化」的量表使用,別逐項照搬。
  • 本書為 1993 年第一版,度量單位以程式碼行數為主。

🔗 相關工具

  • 工具-建構的先決條件——同書。規模越大,先決條件(需求、架構)沒做好的代價越高;本卡的「分析和結構錯誤在大專案佔比上升」(p.356)正是那張卡的成本論證。
  • 工具-估算的藝術——互補。那張卡講「怎麼估」(三點估算、範圍與信心度、估算 ≠ 承諾);這張卡講「估之前要先知道自己在哪一格」,特別是 p.356 那組系統性低估係數(80%/50%/低估近 10 倍)——先用本卡校正基準,再用那張卡包裝成可溝通的區間。
  • 回連 程式碼大全

📌 本章小結(p.358 原文四點)

  • 在一些小專案中的活動並不能想當然地用於大專案中,你應仔細計劃它們。隨著專案增大,建構作用減弱。
  • 隨著專案的增大,交流方法應簡化。各種方法的使用應因時而異。
  • 在相同條件下,大專案的生產率比小專案低一些。
  • 在相同條件下,大專案的每行錯誤數比小專案的每行錯誤數多。