l

2010年3月13日 星期六

敏捷式例外處理設計的第一步:決定例外處理等級

03/13 20:12~23:25

今天 Teddy 想談一下例外處理設計 (exception handling design) 這個問題(PS:終於回歸到 Teddy 的本業...)。N 年前曾經有人研究過,軟體中大概有 2/3 的程式都是用來做例外處理或是錯誤處理。既然例外處理佔了軟體系統那麼大的篇幅,理當受到分析師或是開發人員極大的重視才對。錯!在實務上,相較於討論如何設計正常功能的文獻(書籍,論文,文章,程式範例),關於如何做例外處理設計的討論就少了很多。鄉民們可以回想一下,在求學的過程中,在程式語言的課程或是軟體設計課程中,有多少老師曾經教過你如何設計例外處理,而且你實際用過之後還真的可行?如果有,Teddy 在此先恭喜老爺,賀喜夫人。如果沒有,也沒關係,看看本篇就是一個好的開始。

************

例外處理在軟體開發中面臨的困難

Teddy 本身覺得從整個軟體開發流程的角度來看,要把例外處理做好是件蠻難的工作。為什麼難,簡單可歸類下面三個原因:

  • 屬於非功能性需求的例外處理很容易被忽略:大體上軟體開發的順序,是先從功能性需求 (functional requirements) 開始做起,然後再考慮像是可用性,安全性,強建性 (robustness),易用性等非功能性需求 (non-functional requirements,或稱為 quality attributes)。在軟體開發專案普遍面臨開發時間不夠的問題,因此類似例外處理這些非功能性需求就經常變成被忽略,犧牲的對象。開發人員常常會對自己說:『我先把功能做出來,之後再來處理例外』。可是,通常等功能寫好之後,這些暫時被列為 TO DO 的例外,就永遠被世人所遺忘了。
  • 有些例外是需求面看不到的,要到實做時才會出現:有寫過 use cases 的鄉民們都知到,use cases 有所謂的 failure scenarios,因此在撰寫需求時分析師便可規劃對於這些 failure scenarios 要如何處理。但是,有很多例外是和實做方法或是選擇的軟體元件有關,因此這種與實做相關的例外就沒有被列在需求分析中,而通常依靠 programmers 的良心來辦事。至於 programmers 有沒有良心,套句特偵組發言人的口頭禪:『不便說明』。不過,Teddy 可以確定的是,就算是很有良心的 programmers,也不見得有能力把例外處理做好(請看下一點)。
  • 真的不知道要怎麼處理:寫過 Java 程式的人都知道,呼叫與 IO 相關的 methods 時,會丟出 IOException 這個 checked exceptions (註:Java 的例外和『斯斯』一樣有兩種:checked 和 unchecked。Checked exceptions 表示 compiler 會檢查收到這類例外的人有沒有遵循 catch or declare 這個規則。翻成白話文就是說如果你呼叫某個會丟出 checked exception 的 method ,那麼你只有兩個選擇:寫一個 try block 把這個例外抓下來,或是把這個例外宣告在你自己這個 method 上頭繼續往外丟。Java 對於 unchecked exceptions 就沒有這樣的限制)有沒有什麼通用的設計準則告訴開發人員,當你收到 IOException 時要如何處理?好像沒有(有的話請好心地通知 Teddy 一下)。因此,要如何處理就看 programmers 的良心了。有人會反射性的把例外捕捉之後直接忽略,然後假裝沒事繼續做其他功能;有人把例外往外丟將這個難題交給下一個人;有人會把例外 log 下來然後認為這樣就算是把例外處理好了;有的人捕捉這個例外然後丟出一個新的例外;有的人捕捉例外之後,嘗試修復例外造成的問題,但是通常越補越大洞。簡單的說,在大部分的情況下,由於不知道例外要如何處理,不同的 programmers 通常依據自己的喜好隨機做出決定,而這樣的決定通常會降低程式的強健度。

************


定義例外處理等級

Teddy 要傳授的方法很簡單,就是請開發團隊定義軟體的『強健性等級』(參考 Table 1),之後這個強健性等級就變成例外處理的『需求』。有了需求,programmers 遇到例外的時候就知道要如何處理才能滿足這個需求。有點玄... 好滴,Teddy 先解釋一下這四個強健性等級的含意,再舉例子說明之。
 

G0: Undefined (未定義)

如果你還沒有幫你的程式貼上『強健性等級』,那麼你的程式就屬於 G0 這個等級。G0 表示當某個 service (可以想成整個系統,呼叫某個元件,SOA 中的 service call 或是一般的 function call)  發生錯誤的時候,可能會讓呼叫者知道錯誤發生,也有可能會假裝沒事 (failing implicitly or explicitly)。也就是說使用該 service 的人,其實是無法確切得知它是否有成功達成任務。而當錯誤發生的時候,service 處於不明或是錯誤的狀態 (state)。例外發生時系統可能會終止也可能繼續執行 (terminated or continued)。

G1: Error-reporting (錯誤回報)

G1 表示當某個 service 發生錯誤的時候,一定要讓呼叫者知道,絕對不能假裝沒事 (failing explicitly)。因此,使用該 service 的人便可確切得知它是否有成功達成任務。而當錯誤發生的時候,service 處於不明或是錯誤的狀態。例外發生時系統要終止執行(因為此時狀態已經不明,所以繼續執行下去可能會讓整個系統錯得更離譜,所以要立刻終止)。

要達到 G1 強健度等級很簡單,就是把所有的例外都往外丟,然後在主程式 (main program) 捕捉所有的例外並回報給使用者知道。G1 又稱為 failing-fast。

G2: State-recovery (狀態回復)

和 G1 一樣,G2 要求當某個 service 發生錯誤的時候,一定要讓呼叫者知道,絕對不能假裝沒事 (failing explicitly)。和 G1 不同,G2 要求當錯誤發生之後,service 必須保證還是處於正確的狀態。由於整個系統的狀態還是正確的,因此例外發生時系統可以繼續執行(continued)

要達到 G2 強健度等級就要多做兩件事情。第一件事情就是 service 要想辦法回復到別人呼叫它之前的正確狀態 (error recovery)。例如,如果該 service 修改了資料庫裡面的資料,當例外發生時就要執行 rollback (簡單說就是要 undo  之前修改過的狀態)。第二件事情就是釋放資源 (cleanup)。例如,把要來的記憶體,file handlers,connections 等資源釋放。G2 又稱為 weakly tolerant。


G3: Behavior-recovery (行為回復)

由 G3 的名字鄉民們應該可以猜到 G3 是很有責任感的,要求『使命必達』。因此,當某個 service 發生錯誤的時候,要另外想辦法排除困難,總之就是要達成任務。和 G2 相同,G3 要求當錯誤發生之後,service 必須保證還是處於正確的狀態 (註:唯有在狀態正確的前提下,在例外發生之後想其他方法繼續達成任務才有意義。) 由於整個系統的狀態還是正確的,因此例外發生時系統可以繼續執行(continued)

要達到 G3 強健度等級除了要做到 G2 的 error recovery 和 cleanup 之外,還需要『想其他方法達成原本的任務』。這些其他方法包含 retry,design diversity, data diversity, functional diversity 等等 (google 一下會有這些方法的詳細說明)。G3 又稱為 strongly tolerant。

鄉民甲:等一下,萬一遇到八八水災,或是秘魯大地震,一個 G3 的 service 真的沒辦法達成使命,那怎麼辦?請參考圖1,此時 G3 就會降級變成 G2...依此類推...





************

要怎麼用

重點來了,這些強健度等級要如何使用?以下是 Teddy 實際在團隊中推導此作法的步驟:

  1. 花 1-2 個小時教導強健度等級觀念。
  2. 在沒有特別規定之下,預設所有的 classes/methods 一定要達到 G1。如此一來,在開發階段經由各種測試我們便可盡量找出應該處理而沒有被處理的問題。在做這個規定之前,其實有滿多例外都被忽略了,從使用者介面上看不到錯誤,可是系統的狀態已經不對了,在這種情況下除錯變得更加困難。所以乍看之下 G1 (failing-fast) 好像很不負責的把所有的例外都往外丟,但是反而可以在開發階段發現問題並加以修復(此時便可決定針對該例外是否必須由 G1 提昇至 G2 或 G3),因此整體而言提昇軟體的強健度。
  3. 對於特定的操作,例如資料庫處理,由於有 transaction 可以使用,可以很容易達到 G2,因此剛開始實做時就必須達到 G2。
  4. 除非客戶要求,或是不達到 G3 會變得很難用,否則不會要求程式要達到 G3。 

Teddy 採用此方法兩年來覺的實務上還滿可行的。強健度等級這個觀念不但簡單易懂,而且也很符合 agile 精神。為什麼?因為強健度等級基本上就是秉持『階段性,逐步改善例外處理設計』的精神。在許多情況下,正常功能還沒有全部完成時,是不太容易決定例外處理到底應該做在整個系統的那一層,此時過早,過於精細的例外處理實做不見得有用,反而可能造成時間上的浪費。舉個例子,假設你採用 Scrum 開發某個軟體,而有某個『功能組』(例如,權限管理)需要2 - 3 個 sprint 才可以把全部的『正常功能』做完。當然在做此功能組的第一個 story 就有可能會遇到很多例外,如果此時對於要如何處理這些例外還沒有很清楚的解法時,可以規定這個 story 只要滿足 G1 便可。等這個功能組的全部 stories 都做完,或是等到做完足夠多的 stories,讓該功能組的例外處理變得可以決定之後,再增加一個 story 來提昇這些 stories 的強健度。如此一來,你個客戶也可以知道每一個已經完成的 story 的強健度,甚至可以讓客戶在『增加新功能』與『提昇強健度』等級這兩種不同類型的 stories 做取捨。


友藏內心獨白:寫這一篇所花得時間有點久...

1 則留言:

  1. 兩三年前上過您的例外處理課程,最近的專案終於有機會正式將相關概念與方法介紹給團隊,幾個月前參考了這篇經驗,相當實用,現在團隊已經完全接受例外處理,也很清處下一步改善的方向。非常感謝。

    回覆刪除