l

2013年4月10日 星期三

Clean Code之錯誤處理

Apr. 10 14:38~16:06
螢幕快照 2013-04-10 下午2.44.57
前一陣子太忙,忙到身體有點不適。上禮拜放了幾天假跑到馬祖 因公考察了四天,本以為可以利用機會順便休息一下,但是在馬祖的這幾天都需要以機車代步,加上馬祖都是陡峭的山路,還下著綿綿細雨,機車坐久了還真的挺累人的。不過馬祖風光明媚,戰地風情也別有一番滋味,推薦有興趣的鄉民們去參觀一下。

言歸正傳,話說一個多禮拜沒進辦公室了,今天進辦公室看到桌上放了一本中文版《Clean Code》(無暇的程式碼)。原本博碩文化的陳技術主編邀請Teddy幫忙寫推薦序,但這本書的英文版Teddy一直沒時間讀完,加上中文版出版在即,因此無緣幫本書寫推薦序。但陳主編還是很好心的送了一本中文版《Clean Code》給Teddy,今天剛好利用機會來介紹一下這本書。
***
錯誤處理
拿起書快速翻了一下,看到第7章在講錯誤處理,這剛好是Teddy的「法定專長」,就先從這一章開始談起好了。這一章介紹了幾個錯誤處理的技巧:
  1. 使用例外取代回傳碼(return code)。
  2. 在開頭寫下try-catch-finally敘述。
  3. 使用不檢查例外(unchecked exception)。
  4. 提供例外發生相關資訊。
  5. 從呼叫者角度定義例外類別。
  6. 定義正常程式流程。
  7. 不要回傳null。
  8. 不要傳遞null。

寫到這邊有點快寫不下去,因為本來是要推薦這本書的,但結果可能變成在「吐臭」書中內容…Orz。書中提到有關錯誤處理的這8點技巧,Teddy對於第2點與第3點有點小意見,今天先談一下第2點:在開頭寫下try-catch-finally敘述
首先,在程式開頭就寫下try-catch-finally有時候是辦不到的。請看以下範例,try_catch_finally_V1()這個method的實作使用一個try-catch-finally敘述包起來。
螢幕快照 2013-04-10 下午3.15.25

上面程式看起來很好,沒問題。等一下,FileInputStream用完之後不是應該要關閉嗎?請參考中文版書中第118頁的例子,書中採用類似以下程式碼的作法來關閉串流:
螢幕快照 2013-04-10 下午3.19.20

第143行的程式fis.close()這種寫法其實是一種例外處理壞味道(exception handling bad smell)Teddy將其稱之為Careless cleanup。為什麼?因為cleanup code應該要寫在finally裡面。
螢幕快照 2013-04-10 下午3.22.44

上面這段程式是無法通過編譯,因為fis被宣告在try clause裡面,因此在finally clause存取不到它。所以程式只好改成:
 螢幕快照 2013-05-02 下午1.46.50

也就是要把宣告fis的敘述放在try statement的外面,如此一來也就違反了「在開頭寫下try-catch-finally敘述」的這條建議。除非鄉民們要把程式寫成下面這個樣子,但如此一來程式結構又變成存在nested try statement這個bad smell,感覺也不太好。
螢幕快照 2013-05-02 下午1.47.20

***
請Java 7來幫忙
參考書上的範例,如果要做到「在開頭寫下try-catch-finally敘述」,就必須使用Java 7新的功能。
螢幕快照 2013-04-10 下午3.38.49
請參考上圖第219行程式,把需要關閉的資源寫在try (…)裡面,這樣子當離開try敘述的時候,所使用到的資源會自動被關閉,程式設計師也就不需要在finally中去手動關閉資源。這個功能感覺起來是Java從C#那裏 抄襲 借用過來的,在Java裡面要被自動關閉的資源,必須要實作AutoCloseable這個介面,否則無法自動關閉。
***
沒哪麼嚴格
再仔細讀一下「在開頭寫下try-catch-finally敘述」這條規則的定義,書中第117頁提到:如果你的程式可能會拋出例外,請養成讓try-catch-finally成為開頭敘述的好習慣。從這個角度來看,上面try_catch_finally_V4()這個版本也符合這條規則(因為宣告fis這一行不會丟出例外,所以寫在try statement之外是OK的)。
講了這個多廢話,Teddy想提醒的重點是,可能是書中採用TDD方式來舉例子,所以程式碼中出現了類似下圖第143行的bad smell(因為程式範例還沒有完全的重整)。再強調一次,下圖第143行fis.close()的敘述要放在finally clause裡面,而非try clause裡面。
螢幕快照 2013-04-10 下午3.52.04
***
結論就是,Clean Code的作者Robert C. Martin寫的書有口皆碑,這本書的中文翻譯本看起來也很不錯,有興趣的鄉民們可參考。
***
友藏內心獨白:你這是在推薦還是在翻桌啊挑眉質疑

10 則留言:

  1. clean code與特定技術綁得比較緊,專研深入一點的話,很容易想到各種例外,所以我個人更喜歡clean coder,講得是Robert C. Martin的軟體生涯經驗與感想,或許對programmer的長期幫助更大。:-)
    http://www.amazon.com/Clean-Coder-Conduct-Professional-Programmers/dp/0137081073

    回覆刪除
  2. 經過 Teddy 的提醒,馬上回頭再去看了一下Clean Code,果然是Teddy的"法定專長",看的真仔細。
    如果能看的出這裡的矛盾之處,基本上就是專業級的了,那像我在看的時候就是一面點頭同意後直接矇混過去..
    以C#來說,外面包個using,裡面搞個try-catch似乎也不錯,若把 Try-Catch-Finally 換成 Try-Catch 或是 Try-Catch(-Finally) 可能更能表達出Bob大叔的意圖?
    另外,因為Clean Coder P.18~19頁中談到的專業軟體人員開發所必須熟悉的這些技能,從第1條看下去GoF的Design Pattern都還在背招式,更別說POSA還放在書架上,這要那一天才會成為Bob大叔認可的專業軟體人員啊。嗯,看來參加 Teddy 的課也是不錯的選擇,希望能考慮到南部開課,不然上課都要高鐵來回好麻煩,而且還要坐上不只一次。

    回覆刪除
  3. Hi Mars,

    感謝,找時間去看一下Clean Coder這本書。

    Hi 查無此人,

    說實話我在台北開課每次都不一定會開成,所以我想到南部去要湊滿最低開課人數可能更不容易。不然我也是很願意『全省走透透』的啊 XD。

    回覆刪除
  4. 我是陳主編,很感謝Teddy在blog中討論到這一本書。您也提出了一些爭議點,我個人覺得非常好,也感到欣慰。畢竟不是每個人都敢針對這種等級的書籍,提出批評。各位要知道,敢批評,就必須要做功課才提得出證據。很明顯,您真的看了這本書,光就這一點,我就覺得寄給您這本書真的是非常的值得。
    我雖然不是作者,但也算完整地讀了這本書,在讀的過程中,確實也曾覺得某些地方怪怪的,好像比較適用在某些場合,而非能夠完全適用在各種場合。不過,當我讀到越後面時,我就慢慢地釋懷了。因為即便是對於作者本身來說,他也不認為其中所做的,就已經達到完美的境界。舉例如下:
    第15章(P292),"如果你閱讀得夠仔細,你會注意到在本章中,我推翻了不少自己所下的決定。.................程式重構是反覆try-and-error的行為,最後必然....."。另外,在第一章(P15),作者也提到了,本書的許多建議是具有爭議性的。(這一點,在博碩製作本書之前,知名的部落客91和小朱已經討論過了)
    http://www.dotblogs.com.tw/hatelove/archive/2010/04/14/namingruleguide.aspx

    這本書,我不敢講說有多完美(事實上我也不認為有哪一本書能夠在各個面向都是完美的),畢竟當中有幾章,明顯並非完全由作者獨立寫成(作者也標註出了合著的夥伴。)舉例來說,Teddy提到的第七章錯誤處理,就明顯是作者修整另一位作者Michael Feathers的文章而成的結果。
    甚至,我還懷疑,當中是否有某些文章在原文版製作時,就被編輯或其他作者刪除了。這個部分,您們可能無法想像,但確實,當我拿到這本書的原文素材檔時,裡頭可是有許多不曾出現在原文書中的圖片,因此,很明顯,這本書的最原始模樣並非如此,有許多東西很可能是在校稿時刪除的,至於是誰刪除的,則不得而知。
    不過,即便這本書的作者可能不只一個,但Bob大叔敢掛上自己的名字做為封面唯一的作者,我相信,他至少每個字都看過,當然也很可能在修改後,並未針對修改後的片斷程式碼實際地寫出完整的程式來測試。但我覺得,這本書一直在強調的,是一個觀念,「也就是要把程式寫好,不要打混」,至於要如何寫好程式,他參考了很多人的意見,雖然這些意見應該都是Bob大叔認同的,但卻不見得全部都實際執行過。
    至於Teddy提到的書中案例,我也是覺得,關鍵是在下面這句話
    如果你的程式可能會拋出例外,請養成讓try-catch-finally成為開頭敘述的好習慣
    至於如果要問我,Teddy是在推薦還是在翻桌?我個人的感覺是,算是『書評』。而且,我覺得對於編輯或任何一位作者來說,都應該要有這樣的心態-"互相漏氣求進步"。
    歷史上,賣得最多的書是"聖經",但這麼多年來,聖經也一直被討論,某些地方是否與其他地方相矛盾,或某些地方不科學,或者是否有部分是"偽經",但這些討論並不影響大多數的人對於聖經的認同,甚至很多人對於聖經一念再念,每次都有不同的感受。
    這本書,其實已經不缺人講好話,對於經手過至少百餘本IT書的我而言,Clean Code這本書(至少在被引進翻譯的原文書中)真的已屬上乘之作。不過,真的有很多人(就像鄉民-id查無此人)在看這本書時,常常會被作者拖著走,陷入作者設定的情境中,而不斷點頭同意地看下去。這樣並沒有不好,因為,如此一來,比較容易把書看完。就像是武俠小說、科幻小說,會讓人一直想看下去的原理是一樣的。
    不過光是這樣把書看完,那你能獲得的,大概也就是只有被洗腦而已,所以,我認為Teddy提出來的『書評』,也非常有價值。
    我並不建議各位在沒有讀過該書內容前,就先看Teddy的意見。但我建議各位在讀完整本書的內容後,回頭看看Teddy的『書評』或其他更多人的『書評』,然後再搭配原書來審視,你會發現,唷!原來「溫故」真的可以「知新」啊!
    在此,我也想藉用Teddy的部落格發表一點點編輯的感想,在大夥一窩蜂搶著出Android、HTML5、Xcode工具書的浪潮中,我們公司還願意花時間出版這類書籍,已屬難得。對我而言,更難得的是被我撿到(這本書很多年都沒人拿去翻)。出版後的這兩個月,在天瓏的銷售數據,也令我感到極度地欣慰。
    不論讀者是批評指教還是讚美。其實,能夠引起討論,我就覺得至少我盡到了基本的責任。我想,每一位作者寫書,都是希望讓更多的人能夠看到書中的內容,傳達自己的意念(當然也有少部分人只是想賺點稿費,但Bob大叔肯定不缺這點版稅)。而出版社或編輯的責任就是讓這本書不要石沉大海,淹沒在書海之中。
    最後非常感謝Teddy(無償)讓本書在此曝光,也歡迎各位多多提出意見(也可以回信到出版社的意見信箱),因為能看出更多不恰當的地方,對於讀者才是更有幫助的。最近,我常常看到一些大陸簡體書,針對名著寫了"評注版"。可以的話,我也還蠻有興趣找人來針對這本書寫個"評注版"的。至於Clean Coder,Teddy若有興趣的話,可以連絡我,我這邊有一本(這本書裡面可是沒有程式碼的)。
    這篇回PO我都快打完才看到,原來Teddy在隔天也針對第一章描述的Agile精神發表了另一篇文章,這樣的「書評」夠專業,壞的也說,好的也說。讓讀者充分了解這本書的好與壞,買不買,讀不讀,交由讀者決定,這樣的推薦才是真的推薦。
    說真的,我真的感覺我把書送對人了,我個人特別喜歡那張爛泥巴的圖(作者沒找人畫一張類似的,真是謀菜)。
    PS: Teddy身體要多保重啊,我還希望台灣多點人理解Scrum。

    回覆刪除
    回覆
    1. Hi Simon,

      感謝您留了一則應該是本部落格有史以來最長的留言...XD。『Clean Code』原文的內容很棒,中文版也翻譯的很好,Teddy只要有機會就會推薦朋友們去買一本來看。

      我的博士論文(法定專長)剛好是研究例外處理,所以對於Clean Code裡面錯誤處理這個章節特別有興趣,讀起來也特別有『看法』。嚴格講起來在 Java 程式裡面把 close() 放在 try clause 裡面是一個很常見的 bad smell,不知為何麼書中會有這樣的範例程式,最後只能推論應該是程式還沒有重構完成的暫時結果。

      最後謝謝博碩用心翻了這麼一本好書。

      刪除
  5. try {
    ....
    } finally {
    if (fis != null) {
    try {
    fis.close();
    } catch (IOException ex) {
    ...
    }
    }
    }

    回覆刪除
    回覆
    1. 謝謝幫忙Teddy補充,在 finally 的確是要先判斷 fis 是否為 null,我在文章中的程式碼因為要討論 Clean Code 書中的問題,所以省略了在 finally 對於 fis 的檢查。

      刪除
  6. 我已經買了才看到這篇文章,但很感謝各位專業的分析
    我覺得這些專業觀念是可以互相摩差後讓後輩思考更清晰而更好!
    希望能夠有更多這些正向的書評~我覺得受益良多^U^

    回覆刪除
  7. 關於「在開頭寫下try-catch-finally敘述」的疑問,我認為Bob可能不知道,或是不重視你所提出來的Careless cleanup,因為即使在try_catch_finally_v2的142發生了例外,只要離開function後還是會被GC釋放掉。

    我所認為Bob想描述的寫法如下:

    // 只做錯誤處理
    public void try_catch_finally_v2(String aFileName) {
    try {
    tryToReadFile(aFileName);
    }catch (IOExeception e) {
    throw new RuntimeExeception(e);
    }
    }

    // 只做邏輯處理
    public void tryToReadFile(aFileName) {
    FileInputStream fis = new FileInputStream(new File(aFileName));
    fis.close();
    }

    回覆刪除
  8. 对于「在開頭寫下try-catch-finally敘述」 我的理解是这样的,其实这个问题很容易发现,就是一个method里只有一部分代码被try-catch了。其他的方法都在之外。但是其他代码按照道理也可能出现exception。所以说既然已经用了一个trycatch了。还不如把所有代码都放到里面。然后模糊下exception的级别(这可能是另一个比较大的话题)。保证method输出的简单性。 至于像参数声明 这个无所谓的。主要是有逻辑的地方需要trycatch

    回覆刪除