修復每個 bug 後都要問這 3 個問題
你是否曾經修復了一個 bug ,隨後又發現了一個跟剛修復 bug 有關的 bug ,又或是修復 bug 的方式引起了另一個 bug ?當我修改 bug 時,我會問自己三個問題,以確保我已經仔細考慮了它的意義。每次你認為發現並修改了一個 bug 時,可以使用這些問題來提高生產力和程式碼質量。
這些問題背後的主要思想就是:每一個 bug 都是底層程序的一個不良表現。你必須處理這些症狀,但如果你僅僅是處理這些外在症狀,你就會有永遠解決不完的問題。你應該找到產生 bug 的程序,並且修復這個程序。當你確定究竟發生了什麼和發生這些的原因時,也許你就會明白產生 bug 的基礎程序不是隨機的,而是可控的。
在問這三個問題前,你需要克服面對 bug 的這種天生的抗拒,仔細分析 bug 。檢視程式碼並解釋出錯的原因,從能觀察到的現象開始,向後分析,不斷地問為什麼,直到你可以找到產生 bug 的模式。通常,你該跟同事一起做這件事, 因為解釋你認為會發生的事情,將迫使你面對一些假設——這些程式是做什麼的。
“它溢位了,因為下標J越界了。”
“為什麼?”
“J 是 10,但陣列最大下標為 9。”
“為什麼?”
“J 是一個字串長度,陣列的起始下標是 0,所以字串長度為 1 的最後一個字元的索引是 0。”
找到 bug 後,查詢其他意外情況。檢查程式出錯時主要的程式變數的值,是否可以解釋這些值。
“為什麼 name 是 null?”
“為什麼它總是輸出錯誤資訊呢?”
記錄下你做了哪些操作,發生了哪些變化。你需要知道究竟發生了什麼,這樣做就意味著你時刻有一把標尺和歷史記錄。
當完成這些步驟後,你可以準備問第一個問題了。
1. 其他地方也會出現這個錯誤嗎?
檢視程式碼中使用相同模式的地方,系統地改變模式找出類似的 bug 。
“我還在其他什麼地方使用長度作為下標的嗎?”
“所有陣列的起始下標都一樣嗎?”
“對於一個長度為 0 的字串會發生什麼?”
試著描述這部分程式碼中應該是正確的,但是這些 bug 沒有遵循的規則。尋找這個不變數 [ 1 ]的過程將幫助你找到其他潛在的 bug 。
“起始偏移加上長度減去1就是結束的下標,除非陣列長度為 0”。
對於你發現的每一個 bug ,你都可以解決一批 bug ,這是非常高效的。嘗試用概括性的語言描述這些 bug 也能提升你對程式的理解程度,並幫助您避免在程式中引入更多的 bug 。
2. 這個 bug 後面隱藏著什麼其它的 bug ?
一旦你確定瞭如何修復這個 bug ,你就需要考慮一下修復後會發生什麼。這個執行失敗的語句後面的語句也可能有問題,但是程式還沒有執行到此就不知道有沒有 bug ,或者有些程式碼因為你修復 bug 而第一次出現在程式中,這些程式碼也可能有問題。檢視這些未執行的語句,檢查程式碼中的 bug 。
“下一條語句會正常執行嗎?”
當你在想程式的控制流的時候,可以弄清楚還有哪些地方程式沒有執行到。
“是否有我從來沒有測試過的功能組合?”
在程式中插樁(instrumentation)並不會耗費太多時間,在執行程式各個部分的過程中就可以進行檢查,但是你會驚訝地發現開發者測試過的程式碼還有很多都不能正常執行。
“我可以測試出所有的錯誤資訊嗎?”
注意一個地方的改動可能會引起其他地方的 bug 。一些變數的區域性改動可能會在執行時違反後來的假設。
“如果僅是從 J 中減去 1,當長度為 0 時,後面的語句會運算元組中 -1 位置的元素。”
如果程式已經做了大量改動,就要仔細考慮是否有必要增加另外一個補丁,或者是時候考慮重新設計和重新實現了。
(有時候調 Bug 就是這樣的)
3.我應該做些什麼防止類似 bug 的產生呢?
問問自己如何改變程式設計方法,根據定義避免 bug 的出現,通過改變方法或者工具,經常可以移除整個類的錯誤而不用一個一個的解決 bug 。
從“ bug 是何時引入的”這個問題開始:在程式開發生命週期的哪一個階段可以阻止 bug 的產生?
“設計是沒問題的;我在程式設計時引入了 bug 。”
仔細檢查 bug 產生的原因,考慮 bug 產生時正在執行的程序,並想想怎麼改變它來阻止 bug 的產生。
“將偏移的資料型別和長度分離出來將會在編譯時捕獲這個錯誤。”
“每一個文字項可以用隱藏了下標計算的巨集輸出,然後我就可以一次找到它。”
不要滿足於膚淺的答案。假如你對於一個 bug 的解釋是,“我記不清了”,那還怎麼改進這個過程,讓你不再需要記住它?你可以更改程式語言,使被忽略的細節可以完全隱藏,否則你遺漏的部分會被檢測到從而導致編譯問題。對這個問題域,你可能使用了前處理器或者智慧的編輯器,有預設值,錯誤檢查,智慧提示和快速文件。這個 bug 可能是程式設計團隊溝通的問題,亦或是需要討論的設計衝突。
思考發現 bug 的方式,並問問自己如何能更早發現它。測試怎麼可以更嚴密一些?能否進行自動化測試?是否要新增程式碼實時檢測功能,以便可以及時捕獲錯誤資訊?
“我應該在我的測試單元中嘗試長度為 0 的陣列”。
“我應該進行下標檢查,提前捕獲不符合的下標”。
有必要建立一些系統方法和自動化工具,用於編譯、構建和測試,它們可以減少長時間的除錯和查明具體事實的過程。
這個技巧的應用
養成這樣一種習慣:每當你發現一個 bug 時,問自己這三個問題,甚至你不必等到有 bug 時才使用這三個問題。
在設計和審查過程中,你都可以用這三個問題來處理你得到的每一條意見。審閱意見是潛在的溝通過程的結果,使你可以有所改進。如果你認為讀者評論是錯誤的,比如,你可能會問是什麼使你的文章沒被理解,如何更好地與審稿人溝通。
設計評審和程式碼審查[2]是找出 bug 的強有力手段,你可以對審查過程出現的每一個缺陷都提出三個問題。如果審查徹底,前兩個問題不會出現太多新的 bug ,但第三個問題可以幫助你找到方法,用來避免未來可能會出現的 bug 。
致謝:感謝那些教給我這些經驗,豐富了我生活和工作的朋友和同事。感謝Jay O’Dell、Rick Berman 和 Tom DeMarco 對本文早期版本的寶貴的批評和建議。
參考
[1] Hoare, C. A. R, Proof of a Program: FIND, Comm. ACM 14, 39-45, Jan 1971.
[2] Fagan, M. E., Design and Code Inspections to Reduce Errors in Program Development, IBM Systems Journal 15(3), 1976.
[3] Saltzer, J. H., Repaired Security Holes in Multics, MIT CSR-RFC-5, Feb 27 1973.