from http://javaworld.com.tw/roller/page/qing/20070905
很棒的一篇文章,或許IMMain該做個相依列表,因為我常常改出另一個bug出來。
除錯的工作中,找到臭蟲的真實所在,大概會佔去70%以上的時間。在之前的幾篇文章中,我們把主軸放在如何有效的搜尋出臭蟲的位置。而在本回中,我們則要討論在找到臭蟲之後,如何解決你所發覺的臭蟲。
相 較於在開發的過程中撰寫新的程式碼,除錯時需要撰寫的程式碼數量明顯少的多了。在大多數時候,我們僅僅只是改寫既有的程式碼,偶而會為了修正錯誤時添加新 的程式碼。但是,這並不意謂著修正錯誤需要投入的心力小於撰寫全新的程式碼,相反的,為了修正錯誤而對程式碼進行的調整工作,往往是最耗費心神的。
為什麼修正程式碼是最耗費心神的呢?事實上,軟體開發工作中,對既有的程式碼進行修正都是很危險的一件事,這當然包括因為除錯而對程式碼做的修改。無論是不是要除錯,程式員都應該懷著戒慎恐懼、如履薄冰的心情來看待對程式碼的修改,同時把它視為是一件很嚴重的事。
我 有位朋友,他所工作的公司,對於修正程式碼中的錯誤有一個特殊的規定,就是倘若因為錯誤而要修改某個既有系統的程式碼(即使只更動一行),必須協同三名相 關人員,舉行一次的會議討論,只有在所有與會人員都同意欲修改者所提出的修改方式,並且都在會議記錄上簽名同意後,修改者才能進行修改。
以上我所舉的例子固然是一個十分嚴謹的規範,在我們日常的開發生活中,多半不會遭遇到這種規定,但這個例子也突顯出,必須要十分慎重的對待修改程式碼這個動作。
為 什麼修正程式碼中的錯誤需要如此的慎重呢?這是因為軟體系統本身的高複雜度所導致的。軟體系統中的組成,往往有牽一髮而動全身的特性。很多程式碼之間的相 依性,是人腦所難以處理的。前些日子,我為某個系統加上了一個清理系統閒置資源的功能,這個功能乍看之下是一個相當獨立的功能,因為它只是在系統初始化 時,產生一個獨立的執行緒,並且定期的檢查是否有閒置過久的資源,倘若有便加以清除。可是,在加入這個功能後,我們卻發現,我們所有定期執行的工作(cron jobs)全部都沒有在完成工作後把行程終止掉,因此導致系統殘存一堆沒有被終止的行程。但我所做的修改看起來應該和這個問題沒有什麼相依性呀?問題出在那呢?我所加入執行緒的模組,也會被這些獨立執行的cron jobs所含入,因此,這些cron jobs在執行時,這個清理閒置資源的執行緒,也會被一併啟動。這個系統是用Java寫成的,Java的Thread(執行緒)類別,有一個方法叫做setDaemon(boolean),可以用來設定某執行緒為daemon執行緒。而一個Java行程結束的條件是,當所有非daemon執行緒皆終止時,行程便會結束。對,看出問題在那了呢?我的清理閒置資源的執行緒並未被設為daemon執行緒,造成了即使cron job的工作都完成了,行程仍未自然終止。
在上面的這個例子中,我想表達的是,因為軟體系統的高度複雜性,即使程式員小心翼翼的進行修改,仍然有可能引發預期之外的行為,甚至造成嚴重的錯誤。而這類因為修改而造成預期之外的行為,在軟體開發中,我們時常把它稱為「副作用(side effect)」,而副作用正是修正錯誤時的最大敵人。
當 你費盡千辛萬苦,好不容易找到臭蟲的所在位置,也正確的分析出臭蟲的導因時,在動手修改之前,你得隨時將副作用的可怕之處牢記於心,隨時提醒自己不要冒然 的做出修正的決策。請你務必要仔細的評估所做的修改可能對整個系統造成的衝擊及影響,才進行修改。錯誤的修改方式,或許會解決眼前所遭遇的臭蟲,但卻可能 造成新的臭蟲,甚至是將原本就已經存在但未被發現的臭蟲給召喚出來。為什麼會召喚出早已存在的臭蟲呢?這多半都是因為你所除去的臭蟲遮掩了某些臭蟲,例如 因為某個臭蟲的關係,程式的執行永遠都不會經過某一條路徑,而這些寄生在這條路徑的臭蟲,也就一直都沒有被發現了。但是,當你除錯了這個臭蟲之後,程式的 執行便有機會走到這條臭蟲埋伏的路徑,對你而言,它們也就會突然的蹦現出來。
因此,關於修正臭蟲會有兩個議題需要討論:(1)如何防止副作用(2)如何檢查修正後的系統正確性。如何防止你所做的修改造成對系統的副作用呢?你可以從軟體的架構著手。好的軟體架構能夠降低系統中各模組之間的相依性,也同時降低每次修改程式碼對系統造成的衝擊。近年來,設計模式(designer patterns)已廣為軟體設計者採用,而許多設計模式的目標,便包括了「為了因應變更而做的設計(design for change)」。 想要避免日後修改容易引發副作用的局面,最好在架構設計時,便妥善的運用能降低軟體模組之間相依性及相關性的設計。另外,如果允許(時間及資源充足)的 話,可以在設計及開發的同時,建立各程式組件(例如類別)之間相依性的對照表,描述某個程式組件相依於那些組件。透過這種方式,你可以進行追溯的動作。以 下便是一個簡單的例子:
| 類別名稱 | 相依類別 |
| WatchDog | Façade ThreadPoolManager |
| Façade | SQLManager DBAccessProxy |
從這張表,我們可以建立起反向的追溯表:
| 類別名稱 | 受依類別 |
| DBAccessProxy | Façade |
| SQLManager | Façade |
| Façade | WatchDog |
| ThreadPoolManager | WatchDog |
有了反向的追溯表,當我們要修改任一個類別時,我們便可以透過這張表去追溯有那些類別可能會被影響到。當我們修改的是DBAccessProxy時,便知道Façade可能會被直接影響到,而且還有可能因為Façade被影響到,導致WatchDog也 被間接的波及。我們在設計或撰寫程式時,可以同時建立起正向的相依關係,接著再利用正向的相依關係,建立起反向的受依關係。有了這兩張追溯用的表格,對於 我們評估某個修改的動作,可能會對系統造成的衝擊是有幫助的,因為我們可以藉以了解這個修改的動作之影響所及,究竟包括那些類別,也有助於我們構畫出系統 大致的面貌。另一方面,當你的追溯表龐大而且相依關係交錯複雜時,代表你的系統也許存在著設計上的根本問題。此外,類別之間的相依關係,可以在設計階段建 立,也可以在程式撰寫的階段倚靠自動化的工具來進行。
你 可以透過好的架構設計,跟相依性評估,來降低修改錯誤所造成的副作用。但無論如何,進行修改之後,我們都需要進行測試。測試的目的有二:一是檢查所做的修 改是否的確修正了目標臭蟲,二則是檢查所做的修改是否不正確的改變了系統其餘部份原先就保有的正確行為。所以,除了針對臭蟲回報時所提供的操作情境,重新 複製產生臭蟲的方法,以檢查臭蟲是否已被消除之外,我們通常會花上更多的心力來檢查,系統其餘部份是否依然正確。此種類型的測試活動,一般我們稱之為「迴 歸測試(regression testing)」。迴歸測試的目的在於測試之前已經測試過的系統功能是否在系統變更之後,仍然能夠保有運作的正常。
近來單元測試(unit testing)的觀念已廣為接受。尤其像極限編程(eXtreme Programming)之類的開發方法流行之後,更有許多開發團隊接受在撰寫程式碼之前先行撰寫單元測試案例(test case)的觀念。如果單元測試的案例建立的足夠完整,那麼進行迴歸測試的成本也就可以降低,我們便可以對修正程式碼這項工作,擁有較高的信心。日後,我們會針對單元測試進行專門的探討。
透 過測試,我們可以檢視所做的修正是否解決臭蟲,也可以盡可能確保沒有引發系統的副作用,造成了意料之外的改變。最後,還有一個重要的原則要提醒,也就是一 次只解決一個臭蟲。當你解決一個臭蟲後,請務必把應該要執行的迴歸測試執行完畢,確保此次的修正是正確,而且沒有引發新的臭蟲。千萬不要為了節省迴歸測試 的時間,合併多個修正一起測試。這樣子偷懶的作法,會導致在迴歸測試的過程中所找到的問題,難以界定其真正的導因。除錯,需要有耐心。
沒有留言:
張貼留言