Item 46: Simplify the Suspect Code#
複雜的程式碼很難除錯。太多可能的執行路徑和錯綜複雜的資料流會混淆你的思維。因此,簡化失敗的程式碼通常很有幫助——可以是暫時的(為了讓缺陷突出),也可以是永久的(為了修復它)。
在進行大幅簡化之前,確保所有要修改的檔案都在 version control 下,最好在 private branch 上操作。
暫時簡化:Pruning(修剪)#
目標是移除盡可能多的程式碼同時保持故障存在:
- 移除一大塊程式碼或一個複雜函式呼叫
- 編譯並測試
- 如果仍然失敗,繼續修剪
- 如果故障消失,復原上一步的修剪,縮小修剪範圍後重複
這可以用 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 的複雜演算法