l

2013年11月22日 星期五

Java的try、catch、finally(7):責任分擔

Nov. 14 10:48~13:00

image

 

今天來談一個基本的問題:try、catch、finally這三個block各要負擔怎樣的責任?物件導向設計要解決最基本的問題之一就是責任分派(responsibility assignment),以往責任分派所探討的都在類別(class)與方法(method)的層級上面。要做好例外處理設計,應該要重新審視一下,try、catch、finally block各自應該負擔那些工作。

鄉民甲:這還要問嗎?Java語言不是已經告訴我們,如果程式碼會丟出例外,而且你想處理這些例外,就要把程式碼寫在try block裡面,然後用catch block來捕捉例外並且加以處理。作後,在finally block裡面把使用到的資源給釋放掉。

Teddy:你說的沒錯,這也是一般人所認知的「例外處理」。但請仔細想一下,這樣的解釋是否有助於開發人員真正著手撰寫例外處理程式?就拿catch block所負擔的責任來講:「捕捉例外並且加以處理」。前面這個「捕捉例外」比較沒爭議,但是後面的「加以處理」就有問題了。所謂「加以處理」到底要怎樣處理?

鄉民甲:這個你也不知道喔,很簡單啊,「加以處理」就是呼叫例外物件的 printStackTracr() method把例外給印出來啊。或是把捕捉到的例外轉成另外一種例外型別,然後再往外丟。或是在catch block裡面提供替代方案,取代原本try block的實作。

Teddy:還有嗎?

鄉民甲:大致上就這樣了,還能生出什麼花樣出來?

***

關於這個問題,Teddy以前唸書時在研究例外處理的時候,也是苦思良久。思考的心路歷程就省略,直接說明結論。Teddy認為try、catch、finally各應負擔的責任有:

  • Try
    • Implement requirements (can have alternatives)
    • Prepare state recovery (e.g., make a check point)
  • Catch
    • Perform error and fault handling
    • Report exceptional conditions
    • Control retry flow
  • Finally
    • Release resources and report cleanup failure
    • Drop check points if any

***

Try Block

先來看一下try block的責任,第一點implement requirements(實做需求)這一點很簡單,就是把實做某個功能的正常邏輯寫在try block裡面。所謂的正常邏輯可以有一種以上的實作,也就是說除了預設的實作方式(primary),也可以包含替代方案(alternatives)。

第二點prepare state recovery (為狀態回復做準備)這一點鄉民們可能就比較少遇到。請看下列程式片段,try block裡面的實作會改變物件或是系統的狀態,為了希望例外發生時可以將系統回復到正確的狀態,因此一進入try block立刻產生一個check point(檢查點或是資料備份點),當例外發生的時候,可以在catch block裡面透過這個check point把系統狀態回復到正常狀態。

image

 

其實在try block裡面prepare state recovery,然後在catch block裡面回復錯誤狀態(error handling)也不是什麼新鮮事。有寫過JDBC程式的鄉民應該老早就有這樣的經驗,請參考下列程式範例,con.setAutoCommit(false)相當於告訴connection物件準備一個checkpoint(接下來的操作先不要直接commit),在catch block裡面,con.rollback()這一行就是用來回復錯誤狀態。

螢幕快照 2013-11-14 上午11.53.49

程式來源在此

 

Catch Block

Catch block有三個責任,第一點perform error and fault handling(執行錯誤與缺陷處理)。Error handling和fault handling要分開談,error handling的目的在於如果系統因為例外發生而處於一種錯誤狀態,則需要修正這種錯誤狀態,讓系統回復到可繼續運行的正確狀態。Error handling的例子在解釋try block的時候已經提到了,請參考之前的說明。

Fault handling則是要嘗試排除造成例外產生的根本原因,因為如果光是修正系統狀態而沒有排除造成錯誤的原因,那麼系統繼續執行下去還是很有可能重複發生相同的錯誤。由程式自動來做fault handling是屬於比較困難的工作,因為要事先預知發生fault的原因,才有可能在設計階段把排除fault的方法設計到程式裡面。所以,實務上很多fault handling是交給聰明的「人類」來處理。例如,假設鄉民們正在使用某個文書處理軟體,在儲存檔案時因為同時間有另外一隻程式也開啟了相同的檔案而儲存失敗。這時候文書處理軟體會提示使用者:「該檔案已被其他軟體使用中,請先關閉其他程式。」接著使用者可以選擇「重試」或「取消」存檔的動作。文書處理軟體把fault handling(「請先關閉其他程式」)的責任交給使用者來執行。

Catch block的第二個責任是report exceptional conditions(回報錯誤狀況),類似的例子之前已經出現過很多次了,請看下列程式片段,第107行 throw new InvalidPacketException(“Data Underflow”)就是負責用來report exceptional conditions。

Image (6)

 

Catch block的最後一個責任就是control retry flow(控制重試流程),很多人把catch block當作try block的「備胎」,如果try block執行失敗,則可以在catch block裡面提供一個alternative(替代方案),這也是一般人常見的例外處理作法,請參考下列程式片段。

螢幕快照 2013-11-14 下午12.29.26

但是剛剛Teddy在介紹try block的責任時有提到,提供預設實作方式(primary)與替代方案(alternatives)的責任都屬於try block,所以不應在catch block提供替代方案。上面這種程式寫法Teddy認為是一種例外處理壞味道(bad smell),稱之為spare handler。關於spare handler的詳細討論請參考<敏捷式例外處理設計 (5):我到底哪裡做錯之 spare handler>。

為了讓例外發生之後try block可以有機會執行替代方案,就必須要在Java裡面模擬出retry。下列程式片段就是模擬retry的做法,鄉民們可以先不用管Retry class在什麼麼,總之在catch block裡面的retry.rescue() method(第13行程式碼)就是用來扮演control retry flow的責任。

螢幕快照 2013-11-14 下午12.41.04

 

Finally Block

Finally block有兩個責任,第一是release resources and report cleanup failure(釋放資源並且回報釋放資源失敗的狀況),關於這一點前幾集的內容已經討論很多了,在此就不重複。直接說明第二點:drop check points if any(清除check point)。Try block的第二點責任是prepare state recovery,因此可能會在try block裡面產生一個check point。如果發生例外,這個check point會在catch block裡面用來恢復狀態,用完之後並且會自動被丟棄。但是如果沒有例外發生,則必須在finally block裡面把check point丟掉,否則累積太多check point也會發生系統資源不足的問題。

請參考下列程式片段,finally block裡面的 cp.drop() 就是用來丟棄check point的程式碼。

image

***

以上try、catch、finally的責任,是Teddy「遍覽群書」之後整理出來的看法,鄉民們可以參考一下。Teddy自已的經驗是,有了這樣的責任分派觀念之後,在做裡外處理設計與程式撰寫時,會清楚很多,也比較不容易寫出混亂、易錯、不易看懂的程式。

***

友藏內心獨白:這一系列終於快到尾聲了。

沒有留言:

張貼留言