Bug模式與反模式有關,反模式是指被多次證明是失敗的軟件通用設計模式。這些設計的反面示例是傳統正面設計模式的必要補充。雖然反模式也是一種設計模式,但Bug模式卻是一種和編程錯誤相關的不正確的程序行爲模式。這種關係與設計毫不相關,但是與編寫代碼和調試過程有關。
下面分別對13中Bug模式進行詳細瞭解。
1.Rogue Tile模式
起因:通常是由在程序的不同部分之間複製和粘貼代碼段產生的。
症狀:您認爲已經修正了導致錯誤行爲的代碼以後,程序還繼續出現錯誤。
解決方法&預防措施:
- 提取公共代碼。
- 在封裝代碼和對每個功能元素保持單點控制兩者之間做出折中。
- 靜態類型語言會限制語言的可表達性,因而降低了預防Rogue Tile的效果。通用類型在一定程度上可以彌補Java語言的這一缺陷。
- 必須拋出所有已捕獲或者已聲明的檢測過的異常,這樣的要求意味着無法提取出方法中的某些代碼。
- 面向方面的編程技術——通過在程序的類和函數中加入“方面(aspect)”來組織程序——有助於處理複製的代碼(每個方面對應於程序的全局屬性,例如,在方法中處理檢查過的異常的方式)。
診斷清單:
- 代碼呈現出原先修正過的bug似乎仍然存在的症狀。
- 在一項有很多剪切-粘貼代碼的工作中出現bug。
- 某個類的數值字段類型改變引起了bug。
- 修改過的方法在編譯後,返回一個表面合理但實際錯誤的數值。
- 單獨構建TreeVisitor接口,分別爲所需返回的每個類型創建一個不同的accept()。
- 在運行時,儘管對所有事情都使用了相同的TreeVisitor接口,並在每個Visitor方法調用時插入的類型轉換,但結果還是出現了ClassCastException異常。
2.Dangling Composite模式
起因:在使用遞歸定義的數據類型的代碼報告一個空指針異常。
症狀:在定義遞歸數據類型時,沒有爲一些基本類型定義其所屬類。因此,空指針被插入到各種複合數據類型中去。然後客戶端代碼處理基本類型的方式不一致。
解決方法&預防措施:爲保證一致性,確保基本類型已經過正確地表示和檢查,爲每個基本類型分配各自的類。
診斷清單:
- 出現了一個NullPointerException異常,而且不知道它代表什麼錯誤!
- 在使用遞歸方式定義數據類型的代碼中出現了NullPointerException異常。
- 在定義遞歸數據類型時是否存在問題,使得某些基本類型未確定其所屬類?
- 在將空指針插入不同的符合數據類型時是否存在問題?
- 出去Leaf類,並將Leaf節點用Branch中left和right字段的控制來表示是否可以修復ClassCastException異常,並減免類型轉換的操作?
3.Null Flag模式
起因:沒有檢查調用的方法是否將空指針作爲返回值。
症狀:使用空指針作爲異常條件標誌的代碼段報告一個NullPointerException。
解決方法&預防措施:通過拋出異常報告異常條件。
診斷清單:
- 出現了一個NullPointerException異常,而且不知道它代表什麼錯誤!
- 出去Leaf類,並將Leaf節點用Branch中left和right字段的控制來表示是否可以修復ClassCastException異常,並減免類型轉換的操作?
4.Double Descent模式
起因:部分代碼在每次方法調用中向下訪問了兩層,並且在第二次時調度不當。
症狀:程序在執行數據結構的遞歸下訪問拋出ClassCassExceptions異常。
解決方法&預防措施:把執行強制類型轉換的代碼分解到每個類的單獨方法中去。另一種方法是,檢查不變量以確保強制類型轉換成功執行。
診斷清單:
- 當對數據進行遞歸下訪時,代碼會拋出ClassCastException異常。
- 當遞歸下訪數據時,在一次遞歸調用中發生了多級下訪。
- 創建同一個類的多個不同實例時是否會出現問題?
- 使用instanceof和equals來檢查類和對象的標識符;還有更好的方法麼?
- 怎麼做才能消除連續節點中的0值?
- 在進行了一次方法調用後,無法正常調度代碼。
- 出去Leaf類,並將Leaf節點用Branch中left和right字段的控制來表示是否可以修復ClassCastException異常,並減免類型轉換的操作?
- 爲什麼需要在代碼中對所有不變量進行註釋?
- 使用instanceof檢查來避免ClassCastException異常的缺點是什麼?
5.Liar View模式
起因:測試只是直接檢查了模型的不同方面。
症狀:GUI程序通過了測試集的測試,但是隨後卻表現出本該在那些測試中就已消除的行爲。
解決方法&預防措施:在整體測試的基礎上爲模型和視圖編寫獨立的測試程序。
診斷清單:
- 進行GUI設計時是否存在異常?
- GUI通過了所有測試,但是現在客戶打電話告訴我出現了問題。這是什麼原因?
- 單元測試的確是一種輔助調試的好方法,但真的需要寫那麼多單元測試嗎?
- 測試和代碼的運行行爲爲什麼不匹配?
- 測試和代碼的運行時未獲得一致的結果。測試有錯嗎?
- GUI測試是如何工作的?
- 程序中模型-視圖控制器(MVC)結構可能出錯嗎?
- 通過視圖檢查模型是否存在一個最佳時機?
- 如果無法輕易地實現自動測試GUI,那麼怎樣才能使用測試來檢查代碼?
- 是否應該持續重構方法?
- 要對代碼進行有效的、永久性重構,是否存在其他關鍵要素?
- 據稱Java的Robot類對GUI測試很有效。它對每種實例都有效嗎?
- 非GUI的數據顯示軟件也可以顯示出測試和運行時結果之間的差異麼?
6.Saboteur Data模式
起因:一些內部數據被破壞,可能是語法上或者是語義上的破壞。
症狀:用於存儲和處理複雜的輸入數據的程序在執行某項任務時意外地崩潰,而在執行其他類似任務時卻安好無事。
解決方法&預防措施:對輸入數據作儘可能多的完整性檢查,而且越早越好。對於已經損壞的持久數據,研究這些數據並檢查其完整性。
診斷清單:
- 數據輸入代碼本可工作正常,但一段時間後,儘管執行同樣的任務,卻崩潰了。問題出在哪裏?
- 引起崩潰的數據來自於網絡上的大型數據結構。這是否是問題所在?
- 可能引發數據毀壞的原因是什麼——句法部分還是語義部分?
- 可能引發數據毀壞的原因使什麼——手工編輯或自動生成文件?
- 通過分析數據來檢查數據是否已被毀壞,這似乎是一項很大的工程!這難道不是編譯器應該做的工作?
- 類型檢查是否可以幫助從語義上確定已損壞的數據?
- 如果無法在輸入時檢查數據,那麼能對現有數據做哪些操作?
- 對數據執行迭代操作是否是清除毀壞數據的有效方法?在什麼情況下是?
- 是否應該對數據執行迭代操作,訪問所有數據(像在已部署的應用程序中一樣)?
- 是否總可以在被毀壞的數據引起問題之前發現它麼?如果不能,爲什麼?
- 將一行文本分爲兩個String是否會引起難以發現的、被毀壞的數據?
7.Broken Dispatch模式
起因:重載過程使得未經修改的方法激活了其他方法,而不是您所希望調用的方法。
症狀:重載另一個方法之後,在測試從未修改過的代碼時突然出現錯誤。
解決方法&預防措施:插入顯式的向上類型強制轉換;或者,重新考慮在不同類中提供的方法集。
診斷清單:
- 我重載了一個方法,然後另外一個的方法發生了中斷。這說明什麼?
- 是否有可能因爲方法參數不相匹配?
- 添加新的方法是否會引起其他方法的中斷?
- 如果一個通用的方法的構造函數重載了一個更特殊的方法,會發生什麼情況?
- 是否應該在我的代碼中“佈滿”測試?
8.Impostor Type模式
起因:程序針對各種類型的數據使用帶標記的字段,而不是獨立的類。
症狀:程序以相同的方式來處理不同類型的數據,或者數據與任何指定的類型都不匹配。
解決方法&預防措施:儘可能將概念上不同的數據類型分爲幾個獨立的類。
診斷清單:
- 爲什麼程序把不同類型的數據作爲相同類型處理?
- 代碼無法識別某些數據類型。
- 在特殊的字段中使用標記來區分數據類型,這是否會出現問題?
- 爲什麼應該使用靜態類型系統來區分數據類型?
- 爲避免類型不匹配而採用如下解決方法:使用if-then-else語句模塊來調度合適的類型。這種方法會起作用嗎?
9.Split Cleaner模式
起因:程序的一些執行路徑沒有完成它們應該做的工作:程序未能在適當時刻一次性釋放資源。
症狀:程序未能正確地管理資源,而是泄露或過早地釋放了這些資源。
解決方法&預防措施:把負責釋放資源的代碼移到獲得資源的同一方法中。
診斷清單:
- 代碼發生內存泄露。可能的原因是什麼?
- 爲什麼程序過早地釋放資源(比如數據庫連接)?
- 要釋放資源,是否應該在同一個方法中獲得並釋放資源?
- 爲了確保資源已經釋放,是否應該跟蹤代碼可能執行的每條路徑?
- 爲什麼某些代碼執行路徑並不是在相關時刻一次性釋放資源?
- 是否應該對程序可能擴展的所有方式進行預測併爲其編寫代碼?
- 發現沒有包括正確的釋放代碼的執行路徑,是否應該向此路徑添加釋放資源的代碼?
10.Fictitious Implementation模式
起因:接口中包括大量實現方案無法滿足的不變量。
症狀:當使用某個接口的特定實現方案時,處理此接口的客戶端類發生中斷。
解決方法&預防措施:修改接口實現方案,以包括這些不變量。如果這些不變量尚未寫入接口文檔,則在其中作明確說明。
診斷清單:
- 如何正式地規範定義接口?
- 在實現某個接口時,負責處理此接口的客戶類爲什麼發生終端?
- 是否可以不在接口中顯式記錄不變量?
- 加載帶有其他不變量的代碼時存在哪些缺陷?
- 作爲安全保障,是否應指定可全部被靜態檢查的不變量?
- 是否可以將接口不變量的定義限制爲類型簽名?
- 什麼是斷言?是否存在不同類型的斷言?
- 斷言應該被包含在接口實現方案的什麼位置?
- 包括斷言是否會嚴重增加程序執行的開銷?
- 僅有斷言是否足夠捕獲我希望在接口上定義的所有規則?
- 是否可以用單元測試爲其他的接口不變量提供限制規範?
- 單元測試集是否能檢測實現方案中所有的輸入?
- 對於類型簽名和單元測試,哪一種具備更強的表述性?
11.Orphaned Thread模式
起因:多個程序線程一直等待來自某個線程的輸入,而該線程在拋出一個未被捕捉的異常後就退出程序了。
症狀:多線程程序被鎖定,可以或者無法將堆棧跟蹤打印到標準錯誤。
解決方法&預防措施:把異常處理代碼放到主線程中,告知依賴於該線程的其它線程已出現異常情況。另一種方法是,在退出的線程中放入處理程序,使它向其客戶端傳遞相關信息。
診斷清單:
- 多線程代碼被鎖定,它將堆棧跟蹤打印到標準錯誤。
- 如果只使用單線程的設計會怎樣?
- 對線程的stop()方法是否出現問題?
- 在什麼類型的編程中,更有可能遇到被廢棄的第2個線程?
12.Run-on Initializatier模式
起因:某個類的構造函數並未直接初始化所有的字段。
症狀:在訪問未被初始化的字段處拋出了一個NullPointerException異常。
解決方法&預防措施:在一個構造函數中初始化所有的字段。當沒有更好的值可以使用時,使用特殊類作爲默認值。對於有更好的值可以使用的情況,要包含多個構造函數。當受到其他因素的限制時,請至少包含一個isInitialized()方法。
診斷清單:
- 出現了一個NullPointerException異常,而且不知道它代表什麼錯誤!
- 當訪問一個未被初始化的字段時,出現NullPointerException異常。
- 類中不是所有字段都被初始化。是不是未正確構建構造函數?
- 需要客戶類分多個步驟來初始化實例的構造函數是否存在問題?
- 某個類已經多次添加新的字段。這樣添加字段是否可能會引起不正確的初始化?
- 語句的執行順序是否對程序會產生影響?
- 是否應該說服客戶拋掉舊代碼,重新編寫新代碼?
- 若正在使用原有代碼。是否可以修改構造函數簽名?
- 當使用舊代碼時,控制NullPointerException異常錯誤的最好方法是什麼?
- 在類中使用isInitialized()方法會起到怎樣的效果?
- 如何確保類的實例一直處於正確定義的狀態?
- 如果使用空值填充類字段,是否會對初始化有所幫助?
- 確保某個實例是否已被初始化的快速方法是什麼?
- 應如何避免在新的上下文中出現初始化bug?
- 表示默認值的最好方法是什麼?
- 使用特殊的類來表示默認值是否會引起性能的下降?如果是,爲什麼?
- 是否可直接進行初始化檢查,而無需在運行時執行類型轉換,也不會引起性能的下降?
- 有人認爲包含始終可拋出異常的方法纔是正確的編程方法,但這看起來是否過於笨拙?
13.Platform-Dependent模式
13.1與供貨商相關的bug模式
起因:JVM規範中留有某些內容未被指定,例如,未對尾調用的優化作出要求。較之與版本相關的bug而言,這類bug相對少一些。
症狀:某些JVM出現了bug,而其他JVM上則沒有。
解決方法&預防措施:因問題而異。
13.2與版本相關的bug模式
起因:因特定供應商JVM的某些版本中的bug而引起。較之供應商相關的bug而言,這類bug更爲常見。
症狀:JVM的某些版本可能出現bug,但其他版本則沒有。
解決方法&預防措施:因問題而異。
13.3與操作系統相關的bug模式
起因:不同操作系統中的系統行爲規則可能有所不同。例如,在UNIX中,可以刪除已打開的文件;而在Windows上則不能。
症狀:某些操作系統中可能出現,但是其他操作系統則沒有。
解決方法&預防措施:因問題而異。
- 爲什麼代碼只能運行在某些Java虛擬機上,而不能運行在其他機器上?
- 爲什麼代碼只能運行在JVM的某些版本上,而不能運行在其他版本上?
- 爲什麼代碼只能運行在某些操作系統上,而不能運行在其他操作系統上?
- 與規範相關的bug和與實現方案相關的bug有什麼不同之處?
- 是否存在Java規範和實現方案引起的bug的綜合敘述?
14.用於調試的設計模式
14.1最大化靜態類型檢查
1、儘可能設計final字段。
2、將不可能被改寫的方法設爲final。
3、包括作爲默認值的類。
4、對異常情況進行檢查,以確保所有的客戶端程序都能夠處理異常情況。
5、定義新的異常類型來精確區分各種異常情況。
6、當某個類的實例將一個狀態或固定數目的狀態用於Composite層次結構中的不同子類中時,就要中斷這個類。
7、清除所有可能涉及平臺相關性的行爲。
8、在儘可能多的平臺上進行測試。
9、將類型轉換和instanceof測試降至最少。
10、使用Singleton設計模式幫助最小化instanceof的作用。
11、使用額外的方法和動態調度幫助最小化instanceof的作用。
14.2將引入bug的可能性降至最低
1、提取通用代碼。
2、儘可能實現純功能性方法。
3、在構造函數中初始化所有字段。
4、出現異常情況時立即拋出異常。
5、出現錯誤時立刻報告錯誤消息。
6、通過語法分析、類型檢查等過程儘早發現bug。
7、通過類型轉換、assertTrue()方法、文檔和文檔形式的參數在代碼中置入斷言。
8、儘可能在用戶可觀察到的狀態下測試代碼。
使用上述原則上會有助於減少代碼中的bug的發生機率。您可能會發現,此處討論的部分bug模式不是最經常出現的那些bug;坦言之,本書討論的很多bug模式問題多多,不要指望可一勞永逸地消除這些bug。我們只能學會更快地診斷它們,並使用正確的預防手段清除它們。