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