🎯 什麼情境該想到我

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

書把複雜性的討論放在兩個地方: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 章的檢查表明說「非正式專案裡有些條款根本不必考慮」,該用多嚴的判準要看規模。

其他來源的對照:

回到來源:程式碼大全