【隨筆·技術(shù)】重構(gòu)錯誤處理程式

原網(wǎng)頁


有人研究過,程式中可能會有高達(dá)90%的比率在管理與處理錯誤,Bob大叔在《Clean Code》中談到「許多程式碼[1]完全由錯誤處理所主宰」,90%的比率是真的在管理與處理錯誤的邏輯嗎?還是只是如Bob大叔說的,根本就是散亂的錯誤處理程式碼?商務(wù)邏輯相關(guān)的程式碼需要重構(gòu),對于錯誤處理程式碼的重構(gòu),我們也有許多需要學(xué)習(xí)的地方。

錯誤處理就是一件事

在重構(gòu)或程式碼可讀性的概念中有個共同特性,就是函式(方法)應(yīng)該只做一件事,避免函式中的程式碼陷入邏輯泥塊(Logical clump)。在沒有例外處理的語言中,透過回傳錯誤碼,讓函式客戶端可以檢查執(zhí)行結(jié)果,確認(rèn)后續(xù)是要進(jìn)行正?;蝈e誤處理流程,如果客戶端必須呼叫多個函式來完成一項(xiàng)任務(wù),檢查錯誤碼、正常與多個錯誤處理流程夾雜的情況下,容易使得客戶端程式碼變得混亂。

例外[2]處理機(jī)制可以在錯誤發(fā)生的時候拋出例外,讓錯誤處理能推到想要的邊界進(jìn)行處理。以Java來說,客戶端可以在try區(qū)塊中處理正常流程,在catch區(qū)塊處理呼叫各函式時可能拋出的例外,讓原先糾纏在錯誤處理流程中的正常流程清楚地呈現(xiàn)出來,try區(qū)塊中的流程亦可抽取為函式獨(dú)立地做一件事,那么目前的try-catch就能專心地做錯誤處理這件事,如同Bob大叔說的「函式應(yīng)該只做一件事,而錯誤處理就是一件事」。

有時例外處理流程會形成一種模式,例如涉及資源建立、使用與關(guān)閉的操作若會拋出例外,為了有限資源在各種錯誤發(fā)生時都能確實(shí)釋放,不免要撰寫類似的try-catch-finally流程,在具有受檢例外[3]的Java中更是難以避免這類情況,像是JDBC的處理流程就是如此,此時可以采用樣版回呼(Template callback)模式,適當(dāng)?shù)刈屬Y源相關(guān)操作從錯誤處理流程中獨(dú)立出來,Spring的JdbcTemplate就是這類實(shí)現(xiàn),因?yàn)檫@類資源建立、關(guān)閉的操作模式太頻繁出現(xiàn),JDK7就提出了try-with-resources語法來解決這類需求,確實(shí)地讓資源建立、使用與關(guān)閉的操作與錯誤處理分離,若進(jìn)一步地結(jié)合JDK8的Lambda語法,還可讓資源的使用從建立與關(guān)閉中分離。例如設(shè)計一個open方法,就可以專心在FileInputStream的使用,讓開啟檔案的意圖顯而易見:

open(fileName, fileInputStream -> {
    // 操作FileInputStream實(shí)例
});

多個捕捉做相同處理時的重構(gòu)

如果多種例外捕捉后,做的都是相同的錯誤處理,像是日志,或者是將程式庫的例外封裝為自定義例外等,錯誤處理的程式碼必然就出現(xiàn)重復(fù),自然就會呈現(xiàn)需要重構(gòu)的訊號。因?yàn)槎喾N例外做的都是相同的事,可將有繼承關(guān)系的例外處理程式碼,合并在父類別的捕捉區(qū)塊中,但不建議使用catch-all的方式,例如使用ExceptionThrowable來捕捉所有例外,因?yàn)閷τ谄渌幌嚓P(guān)的例外,這是一種隱藏錯誤的做法。

然而在合并有繼承關(guān)系的例外處理程式碼之后,仍會發(fā)現(xiàn)沒有繼承關(guān)系的例外處理程式碼出現(xiàn)重復(fù),Bob大叔在《Clean Code》中提出的作法是包裹呼叫的API,確保它在捕捉各種例外后,能轉(zhuǎn)換為(自定義的)共同例外型態(tài),如此客戶端就只需要捕捉一種例外,因而可讓客戶端程式碼大幅簡化,如果使用的是第三方API,也可以同時降低了對它的依賴。

如果多種例外在捕捉之后,做完相同處理就將原例外重新拋出,可以參考guava-libraries的作法,你可以使用catch-all的方式捕捉各種型態(tài)的例外,做完相同錯誤處理之后,使用Throwables.propagateIfInstanceOf以指定的例外型態(tài)重新拋出(通常是受檢例外),或者是使用Throwables.propagate,將原例外以RuntimeException包裹后重新拋出,既消除了重復(fù)的錯誤處理程式碼,又避免了隱藏錯誤。

雖然實(shí)際上,Throwables.propagateIfInstanceOf只是將型態(tài)判斷與轉(zhuǎn)型的邏輯封裝并予以重用,但對客戶端程式碼的簡化確實(shí)有所幫助,不過,這種方式對于錯誤處理時進(jìn)行例外型態(tài)轉(zhuǎn)換,或者是不重新拋出的情況并不適用,guava-libraries的〈ThrowablesExplained〉文中也解釋了其他一些不適用的場合。JDK7中,對于多個捕捉做同一件事的情況,提出了Multi-catch語法,算是為這問題提出了較好的解決方案。

多個捕捉做不同處理時的重構(gòu)

如果多種例外捕捉后,分別進(jìn)行不同的錯誤處理,此時得檢視多種例外是由單一方法拋出,或多個方法操作而分別拋出不同例外,最常見的情況是一個try區(qū)塊進(jìn)行了數(shù)個會拋出例外的操作,然后底下連續(xù)多個catch區(qū)塊逐一針對不同例外作處理。實(shí)際上每個會拋出例外的方法發(fā)生錯誤時,理由應(yīng)該是各不相同的,應(yīng)試著讓這些方法各有一個try-catch區(qū)塊,讓每個方法的錯誤處理流程各自顯露出來。

一旦你根據(jù)不同方法引發(fā)的例外,將一個try搭配多個catch的程式碼,分解為數(shù)個try-catch區(qū)塊之后,應(yīng)當(dāng)立即想到「錯誤處理就是一件事」,而兩個以上的try-catch時,無論那些try-catch是形成巢狀或者是瀑布式流程,都意謂著你的程式碼做了兩件以上的事,重構(gòu)的方式之一,就是每個try-catch重構(gòu)至獨(dú)立的方法之中,讓每個方法都只會出一個try陳述。

當(dāng)發(fā)現(xiàn)一個方法中會出現(xiàn)多個try-catch時,而每個try-catch都做類似模式(但細(xì)節(jié)不同)的轉(zhuǎn)換或錯誤處理時,如果你接觸過函數(shù)式的錯誤處理風(fēng)格,例如我先前專欄〈函數(shù)式風(fēng)格錯誤處理〉中談過的Option、Either、Try等概念,就有可能進(jìn)行Monad風(fēng)格的錯誤處理,我在專欄〈神秘的Monad不神秘〉中談到OptionalflatMap可連續(xù)處理null與物件值轉(zhuǎn)換的問題,實(shí)際上,Mario Fusco在〈Monadic Java〉中就以類似風(fēng)格,設(shè)計了Validation等類別,可以用Monad風(fēng)格對使用者進(jìn)行如下的程式碼驗(yàn)證與驗(yàn)證失敗訊息之收集,而又不會迷失在瀑布式的ValidationException捕捉程式碼之中:

Validation<List<String>, Person> validation = success(person)
    .failList()
    .flatMap(Validator::validAge)
    .flatMap(Validator::validName);

重構(gòu)是看待錯誤處理的一個角度

既然程式中可能會有高達(dá)90%的比率在管理與處理錯誤,我們真的該認(rèn)真且從不同角度去看待,像是受檢或非受檢例外的運(yùn)用、例外應(yīng)捕捉或拋出、避免隱藏錯誤、換個典范風(fēng)格思考錯誤處理的可能性等,都該有所思考,我的專欄〈Shit Happens!該抓還是該丟?〉、〈避免隱藏錯誤的防御性設(shè)計〉與〈函數(shù)式風(fēng)格錯誤處理〉都曾做過一些探討。

從重構(gòu)角度出發(fā)來看待錯誤處理程式碼,你會發(fā)現(xiàn)Martin Fowler的《Refactoring》中揭露的重構(gòu)原則,對待錯誤處理程式碼也是適用的,錯誤處理之所以重要,就在于它是處理不對的事情,本身必須正確,然而就如Bob大叔說的「如果它糢糊了原本程式碼的邏輯,那就不對了」。


  1. 代碼 ?

  2. 異常 ?

  3. Checked Exception ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,854評論 18 399
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,355評論 25 708
  • 不管使用的是哪種語言進(jìn)行程序設(shè)計,都會產(chǎn)生各種各樣的錯誤。Java提供了強(qiáng)大的異常處理機(jī)制。在Java中所有的異常...
    殘月雨紛紛閱讀 1,311評論 0 2
  • 在這個競爭激烈的時代,毫無疑問現(xiàn)在很多公司都在想著如何去完善自己的品牌,讓自己的品牌發(fā)聲,讓品牌成為產(chǎn)品的代言詞,...
    51運(yùn)營閱讀 411評論 0 1
  • 我住院的第三天,苗天華他們?nèi)齻€都來到了醫(yī)院看我,他們的神情嚴(yán)肅。眼中充滿了凝重之色。 “我說,你們一個個這么嚴(yán)肅,...
    浮生萬夢星耀燭天閱讀 182評論 0 1

友情鏈接更多精彩內(nèi)容