l

2013年5月9日 星期四

讀Clean Code有感:例外處理程式重構篇

May 6 22:24~23:16

image

 

前一陣子在讀Clean Code這本書,讀著、讀著突然想到一件事,幾年前Teddy還在唸書的時候,曾經提出一個在Java/C#語言中使用try statement的用法,就是:

One method, one try statement. (一個method裡面只能有一個try敘述)

這個想法是Teddy從Eiffel語言的例外處理做法得到的靈感,Eiffel的例外處理是透過rescue、retry來達成,細節就不管它了,總之一個method裡面只能有一個rescue敘述(可以想成Java的catch(Throwable e))。

image

***

當年Teddy只是直覺認為在Java裡面如果也可以遵守一個method裡面只有一個try statement,應該可以讓method正常邏輯與例外處理邏輯變得更簡單且易懂。有一點像是single responsibility principle一樣的道理,一個method如果需要一個以上的try statement,可能隱含這個method負擔太多的責任。但是當時Teddy並沒有特別花時間去把這個想法做更進一步的整理。

最近讀了Clean Code之後,突然想到把之前寫的例外處理範例程式拿出來整理一下。請看最原始的版本,這個method並沒有做例外處理,而是把IOException直接往外丟。

螢幕快照 2013-05-06 下午10.42.16

***

經過分析之後,發現這個method做了兩件事,這兩件事都可能發生例外。首先,這個method從一個DataInputStream讀出一個整數,這個整數代表某個資料封包的長度。第52行的程式aIS.ReadInt()可能發生IOException,如果發生例外,則表示讀取資料長度錯誤,因此丟出一個InvalidPacketException例外,並且註明「Read data length error.」。

螢幕快照 2013-05-06 下午10.56.54

 

第二件事就是連續讀取N個byte,把資料封包的內容讀出來。程式61行可能會發生兩個例外,分別是EOFExceptionIOException。前者表示資料長度不夠,後者表示讀取資料內容發生錯誤。所以這個method一共有三個狀況會發生例外,雖然對外都是丟出InvalidPacketException,但是錯誤的原因各不相同

***

套用One method, one try statement這個設計原則,以及簡單的extract method這個refactoring,再把上面這個程式改成:

螢幕快照 2013-05-06 下午11.02.13

 

不知鄉民們有沒有覺得改過的程式不但變得比較易懂,對於例外處理的邏輯也清楚很多。

***

友藏內心獨白:有人說,例外處理的文章都看不懂啊…Orz。

9 則留言:

  1. 回覆
    1. 我在部落格上面有寫了好幾篇例外處理重構的文章,您有興趣也可以參考一下。

      刪除
  2. 自己最近寫的程式也是覺得method越單純越好

    回覆刪除
    回覆
    1. 以前會怕說 method 太短,但近來慢慢覺得可讀性還是比較重要。短一點的 method 責任比較清楚,而且可以用 method 名稱達到『說明程式目的』的效果,也比較好理解與維護。

      刪除
  3. 回覆
    1. 謝謝,裡外處理是Teddy的『法定專長啊』 XD。

      刪除
  4. 有一點點疑問想請教

    1. single responsibility 與 exception 是兩碼子的事,用exception來觀察single responsibility,我認為有很大的機會陷入盲點,因為 exception很好觀察,但是single responsibility卻不是

    2. 包 new exception 感覺沒有必要性,除非真的想知道死在哪,通俗點講,像是在脫褲子放屁?

    使用匿名發表是因為功力還不夠,但抱持不同觀點的一點點拙見,也請 teddy大 賜教 :)

    回覆刪除
    回覆
    1. 我最喜歡別人來發表不同的意見...XD

      (1) single responsibility 原本是說一個『類別』只做一件事情 (或是你只會因為一個原因去修改一個類別),把這個原理套到 method 身上,我認為也是適用的。這個原則難就難在『如何定義什麼才算一件事情的範圍』。文中 fetchRawBytesAndSetupMessageV4() 這個例子,從例外處理的角度,剛好發現這個 method 做了兩件事(a)讀 message 的長度(b)讀 message 的 body。這兩件事都可能會失敗(failure),也都會丟出例外。你可以試看看,相同的程式碼如果只用一個很大的 try statement 包起來,例外處理會變得困難很多。

      (2) 你指的 new exception是說『new InvalidPacketException』嗎?如果只丟出 IOException,caller 並不知道 fetchRawBytesAndSetupMessage() 失敗的原因,最後這個 IOException 傳到 UI 層(或是被寫到 log),也很難分析錯誤原因。從 fetchRawBytesAndSetupMessage() 的 caller 角度來看,收到一個 InvalidPacketException,讓 caller 可以針對『封包格式錯誤』加以處理,例如如果是封包格式錯誤,可以要求 retry,或是顯示『對使用者已意義的錯誤訊息』。

      PS1:我將例外處理分成三個等級(RL1, RL2, RL3),如果是 RL1等級,的確是不需要丟出 InvalidPacketException。在我的例子裡面是為了讓 caller 有機會可以達到 RL3,(就是想要知道『真的死在哪裡,以及死掉的原因』),所以需要丟出一個比較明確的 InvalidPacketException。

      PS2:今年(2013)7月6、7(六、日)我會開一班『例外處理設計與重構實作班』課程,對例外處理有興趣歡迎來報名參加 ^_^。

      刪除
    2. 感謝回復,也請T大繼續在軟工上造福大家

      刪除