Clean Code 系列之異常處理
先前已經對異常如何設計,如何實踐異常都寫了幾篇闡述了。再一次從 Clean Code 角度來談談異常的使用。
1、使用異常替代返回錯誤碼
爲什麼?是從函數的角度去考慮:
函數要麼做什麼事,要麼回答什麼事,但二者不可得兼。也就是修改某對象狀態,或者是返回該對象的有關信息。也就是指令與詢問分隔開來。
如
boolean set(String attribute,String value);
該函數設置某個指定屬性,如果成功,就返回 true,如果不存在那個屬性,就返回 false。
if(set("website","zhuxingsheng.com")){
//
}
但從讀者角度考慮一下,它是在問 websit 屬性值是否之前已經設置爲 zhuxingsheng.com,還是在問 websit 屬性值是否成功設置爲 zhuxingsheng.com 呢?從該行語句很難判斷其含義,因爲 set 是動詞還是形容詞並不清楚。
作者本意是,set 是一個動詞,但在 if 語句的上下文中,感覺它是一個形容詞。該語句讀起來像是在說 “如果 websit 屬性值之前已經被設置爲 zhuxingsheng.com”,而不是 “設置 websit 屬性值爲 zhuxingsheng.com,看看是否可行,然後...”。
要解決這個問題,可以將 set 函數重命名爲 setAndCheckIfExists,但這對提高 if 語句的可讀性幫助不大。真正的解決方案是把指令與詢問分隔開來,防止產生混淆:
if(attributeExists("website"){
setAttribute("website","zhuxingsheng.com");
}
在《領域服務是拋出異常還是返回錯誤碼》[1],提到過如何編寫返回錯誤碼
if(deletePage(page)) == OK){
}
但這樣,從指令式函數返回錯誤碼,有些違反指令與詢問分隔的規則。
雖然這兒沒有像上面的示例一樣,引起動詞與形容詞的混淆,卻會導致更深層次嵌套結構
if(deletePage(page) == OK){
if(deleteRefrence(page.name) == OK) {
if (deleteKey(page.name.key()) == OK) {
//
} else {
//
}
} else {
//
}
} else {
//
}
使用異常替代錯誤碼,錯誤處理代碼能從主路徑代碼中分離出來:
try {
deletePage(page);
deleteRefrence(page.name);
deleteKey(page.name.key());
} catch (Exception e) {
}
抽離 try/catch 代碼塊
try/catch 代碼塊醜陋不堪,搞亂了代碼結構,把錯誤處理與正常流程結構分離開來。
void delete(Page page) {
try {
deltePageAndAllReferences(page)
} catch(Exception e) {
log;
}
}
正常流程結構:
void deletePageAndAllReferences(Page page) {
deletePage(page);
deleteRefrence(page.name);
deleteKey(page.name.key());
}
這樣子代碼乾淨了些,而且函數只幹一件事。錯誤處理就是一件事。
想要更簡化一下 try/catch 代碼塊,可以使用 vavr 工具包中的 Try 類
Try.of((page) -> deltePageAndAllReferences(page)).onFailure(e -> log(e));
ErrorCode 枚舉類
返回的錯誤碼,我們常會使用一個常量類或者枚舉定義所有錯誤碼。
當新增邏輯需要增加新錯誤碼時,就會增加新代碼,而且還要來修改這個錯誤碼類。
這樣的類被稱爲依賴磁鐵,當這個類修改時,其他所有類都需要重新編譯和部署。
使用異常類代替錯誤碼,新異常可以從異常類派生出來,而無須重新編譯或重新部署。
2、使用未檢查異常
在之前的異常文章中,提到檢查異常有很強的穿透力,當類調用鏈路長,在底層方法上增加新檢查異常就會導致上層所有方法修改聲明,有點違反 OCP。
3、異常防腐
在 DDD 中有防腐層的概念,通過防腐層去隔離兩個界限上下文的變化。
異常也有類似的情況。
當調用第三方 API 時,會需要處理異常情況。
ThirdPartAPI third = new ThirdPartAPI();
try {
third.open();
} catch (Third1Exception e) {
//
} catch (Thrid2Exception e) {
//
} catch (Third3Exception e) {
//
} finally {
}
首先我們需要打包這個第三方 API,降低對它的依賴;也不必綁死在某一特定供應商 API 上,定義自己的 API 還要抽象異常
class ThirdPartService {
public void open() {
try {
third.open();
} catch (Third1Exception e) {
//
throw new SelfException(e);
} catch (Thrid2Exception e) {
//
throw new SelfException(e);
} catch (Third3Exception e) {
//
throw new SelfException(e);
} finally {
}
}
}
上面代碼,定義了抽象的 ThirdPartSevice, 並且抽象出 SelfException。
碼農戲碼 遊戲碼農戲代碼,財富自由遊天下
總結
經過上面的三種手法,可以讓代碼在處理異常時,更加整潔。
References
[1]
《領域服務是拋出異常還是返回錯誤碼》: https://www.zhuxingsheng.com/blog/does-the-domain-service-throw-an-exception-or-return-an-error-code.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/vB4YpIFZkR1kPbvQ6TTGZQ