Item 46: Simplify the Suspect Code#

複雜的程式碼很難除錯。太多可能的執行路徑和錯綜複雜的資料流會混淆你的思維。因此,簡化失敗的程式碼通常很有幫助——可以是暫時的(為了讓缺陷突出),也可以是永久的(為了修復它)。

在進行大幅簡化之前,確保所有要修改的檔案都在 version control 下,最好在 private branch 上操作。

暫時簡化:Pruning(修剪)#

目標是移除盡可能多的程式碼同時保持故障存在:

  1. 移除一大塊程式碼或一個複雜函式呼叫
  2. 編譯並測試
  3. 如果仍然失敗,繼續修剪
  4. 如果故障消失,復原上一步的修剪,縮小修剪範圍後重複

這可以用 binary search 系統化地進行。

避免用註解來停用程式碼——巢狀的註解會造成問題。改用以下方式:

// 使用 preprocessor conditionals (C/C++)
#ifdef ndef
  // code you don't want to be executed
#endif
// 使用 false conditional (其他語言)
if (false && b() && !c() && d() && !e())
  someOtherComplexCode();

永久簡化:拆解複雜語句#

將所謂的 train wreck(火車殘骸)語句拆解為可觀察的步驟:

// 難以除錯的 train wreck
p = s.client(q, r).booking(x).period(y, checkout(z)).duration();

// 拆解後,每個中間結果都可觀察
Client c = s.client(q, r);
Booking b = c.booking(x);
CheckoutTime ct = checkout(z);
Period p = b.period(y, ct);
TimeDuration d = p.duration();

這種拆解不太可能影響效能——現代 compiler 非常擅長消除不必要的暫存變數。

永久簡化:拆分大型函式#

一個大函式拆分為多個小函式。好處在於可以個別測試每個部分(見 Item 42),同時改善你對程式碼的理解並解開部件之間不必要的交互作用。

永久簡化:放棄複雜演算法#

有時候,導致 bug 的複雜度根本不是必要的

  • 處理速度提升使某些優化變得無關緊要(3MHz VAX 上 500ms vs 5ms 的差異,在 3.2GHz i5 上變成 500us vs 5us)
  • 記憶體和磁碟容量增大讓複雜的 bit packing 操作不再必要
  • 硬體技術改變使某些演算法過時(SSD 使磁碟 elevator algorithm 毫無意義)
  • 演算法的功能可能已存在於標準函式庫或成熟的第三方元件
  • 過度工程化的效能優化可能從一開始就過度設計
  • 現代 UX 設計偏好更簡單的互動模式

重點回顧#

  • 選擇性地修剪大量程式碼,讓缺陷更明顯
  • 將複雜語句或函式拆分為更小的部分,以便監控或測試
  • 考慮用更簡單的演算法替換有 bug 的複雜演算法