Java 代碼中幾類典型的 "壞味道"

本文首發於個人微信公衆號《andyqian》, 關注即可獲得一線互聯網內推!

前言

最近一段時間進行了部分代碼的code review。其中有 review 的,也有被review的。在這過程中發現了許多問題,而其中就包含許多本不該發生的。同樣的,這些不該發生的問題如果攜帶上線,隨之而來的則是一個個的生產事故。對於金融系統來說,會直接造成資產損失,而對於醫療軟件而言,甚至涉及生命安全。經過分析分類後,以下幾點是最爲常見的問題:

代碼順序倒置

代碼順序,這一點太容易忽略了,以至於有時讓我們覺得這根本不是問題。話不多說,我們先來看例子:

反例

public void createUser(String telephone, String name, Integer age){
    //1. 校驗手機號
    if (StringUitls.isBlank(telephone)) {
        ...
    }
    //2. 查詢用戶
    User uesr = queryUser(telephone);

    //3. 校驗name ...
    if (StringUitls.isBlank(name)) {
        ...
    }
    //4. 校驗年齡
    if (null == age) {
        ...
    }

    //5. 保存用戶
    saveUser(telephone, name, age);
}

上述代碼中存在若干問題,有以下幾點:

  1. 代碼順序混亂,其中 查詢用戶 不應該在第二步,爲什麼呢?因爲如果 name 爲空 或 age 爲空時,則會白白浪費一次數據庫查詢,而這次查詢,原本是不該發生的。
  2. 校驗邏輯和核心邏輯混在一起,造成紅花與綠葉不分。

正例

public void createUser(String telephone, String name, Integer age) {
    //檢查參數
    checkCreateUserParams(telephone, name, age)
    // 保存用戶
    saveUser(telephone, name, age);
}

//校驗參數
private void checkCreateUserParams(String telephone, String name, Integer age) {
    if (StringUitls.isBlank(telephone)) {
        ...
    }
    if (StringUitls.isBlank(name)) {
        ...
    }
    if (null == age) {
        ...
    }
}

//保存用戶
private void saveUser(String telephone, String name, Integer age) {
    User uesr = queryUser(telephone);
    if (user == null) {
       saveUser(telephone, name, age);
    }
}

建議:實際上,工作中編寫的邏輯會比上述複雜很多,但道理是相通的。代碼合理有效順序,也會帶來諸多好處,例如:減少IO操作(調用DAO方法),較少數據傳輸(調用RPC方法)。而合適的順序應該遵循:參數校驗 優先於 核心流程處理

無意的 NullPointerException

我們都知道 NPE (NullPointerException) 的判斷是調用者的職責。而在方法實現中,有很多同學稍微不注意,就會導致一些低級的 NPE (NullPointerException) 產生。例如:

反例

public void processUserInfo(String telephone) {
    User uesr = queryUser(telephone);
    if (user != null) {
       ...
    }
    Long uerId = user.getOid;
}

問題

此時當 telephone 查詢的用戶不存在時,則會產生 NullPointerException,你還別笑,這類問題發生的頻率還不低,非常值得我們注意。

正例

public void processUserInfo(String telephone) {
    User uesr = queryUser(telephone);
    if (user == null) {
       return;
    }
    ...
}

職責模糊

在學習設計模式時,第一條設計原則就是:單一職責。這給我們的啓示是:在定義方法時,應該儘量原子,讓其承擔單一職責。但在實踐過程中,則出現許多比較怪的實現,以下面代碼爲例:

反例

//1. 調用創建用戶
public void invokerCreateUser(String telephone, String name, Integer age) {

    if (StringUitls.isBlank(telephone)) {
        ...
    }
    if (StringUitls.isBlank(name)) {
        ...
    }
    if (null == age) {
        ...
    }
    createUser()

}

//2. 創建用戶原子方法
public void createUser(String telephone, String name, Integer age) {
    User uesr = queryUser(telephone);
    if (user == null) {
       saveUser(telephone, name, age);
    }
}
  • invokerCreateUser 爲創建用戶調用方。
  • createUser 爲創建用戶提供方。

問題:

  1. invokerCreateUser 作爲 調用方承擔了 本該 createUser 提供方承擔的職責。

建議:

無論在什麼場景下,方法的提供方都需要負責進行參數的校驗,而不是將這些職責委託到調用方。否則諸多調用方都需要進行參數驗證,代碼壞味道隨即而來!同樣的道理,思維發散開來。我們在定義方法時:屬於該方法的職責,我們一個也不能少的承擔,不屬於該方法的職責,一個也不需要承擔。

方法過長

抽取方法這點非常重要,好處包括但不限於以下幾點:

  1. 方法越小,職責越單一,其複用性越高,從而減少重複代碼的產生。
  2. 代碼邏輯清晰,可以使主幹代碼更清晰,易於維護。

有很多同學,習慣在一個方法中編寫成百上千行的代碼彰顯自己的能力。但事實上這樣的代碼及其不易維護,且閱讀都非常吃勁。那多少行纔是最合適的呢?這一點,在阿里巴巴的《Java手冊》中提到:

推薦:單個方法的總行數不超過 80 行。(除註釋之外的方法簽名、 左右大括號、方法內代碼、空行、回車及任何不可見字符的總行數不超過 80 行。)

同樣也給出了這樣做的原因:代碼邏輯分清紅花和綠葉,個性和共性,綠葉邏輯單獨出來成爲額外方法,使主幹代碼更加清晰;共性邏輯抽取成爲共性方法,便於複用和維護。

以下述方法爲例:

反例:

public void createUser(String telephone, String name, Integer age){
    //
    if (StringUitls.isBlank(telephone)) {
        ...
    }
    if (StringUitls.isBlank(name)) {
        ...
    }
    if (null == age) {
        ...
    }
    //用戶不存在則保存
    User uesr = queryUser(telephone);
    if (user == null) {
       saveUser(telephone, name, age);
    }
}

正例:

public void createUser(String telephone, String name, Integer age) {
    //檢查參數
    checkCreateUserParams(telephone, name, age)
    // 保存用戶
    saveUser(telephone, name, age);
}

//校驗參數
private void checkCreateUserParams(String telephone, String name, Integer age) {

    if (StringUitls.isBlank(telephone)) {
        ...
    }
    if (StringUitls.isBlank(name)) {
        ...
    }
    if (null == age) {
        ...
    }
}

//保存用戶
private void saveUser(String telephone, String name, Integer age) {

    User uesr = queryUser(telephone);
    if (user == null) {
       saveUser(telephone, name, age);
    }
}

無用代碼引用

去除無用代碼,其實有很多方面都是需要注意的,其中包括但不限於以下幾點:

  1. 去除未使用的 import。(window 可用快捷鍵:Ctrl+Alt + O 進行格式化)。
     

 

  1. 去除未使用的局部變量。

反例

private void checkCreateUserParams(String telephone, String name, Integer age) {
    //1. 未使用的局部變量 sex
    Integer sex = "";
    if (StringUitls.isBlank(telephone)) {
        ...
    }
    if (StringUitls.isBlank(name)) {
        ...
    }
    if (null == age) {
        ...
    }
}

正例

private void checkCreateUserParams(String telephone, String name, Integer age) {

    if (StringUitls.isBlank(telephone)) {
        ...
    }
    if (StringUitls.isBlank(name)) {
        ...
    }
    if (null == age) {
        ...
    }
}

3. 移除未使用的方法

 

反例

private void checkCreateUserParams(String telephone, String name, Integer age) {

    if (StringUitls.isBlank(telephone)) {
        ...
    }
    if (StringUitls.isBlank(name)) {
        ...
    }
    if (null == age) {
        ...
    }
}

// 未使用的私有方法
private void check(){
    return ;
}

正例:

private void checkCreateUserParams(String telephone, String name, Integer age) {

    if (StringUitls.isBlank(telephone)) {
        ...
    }
    if (StringUitls.isBlank(name)) {
        ...
    }
    if (null == age) {
        ...
    }
}

建議:

  1. 代碼中未使用到的代碼,及時刪除,特別是私有方法。已經上線的RPC方法,則採取版本遞進的方法進行移除。未上線且未使用的RPC方法及時刪除,否則上線完後刪除的風險就會非常大。

掛羊頭賣狗肉

這一類問題,其實在接口定義上出現的比較多。其簡單來說:就是表裏不一,方法中返回對象,方法名,方法請求對象三者不統一。這樣的接口提供給調用者,調用者會非常疑惑,甚至想過來懟你兩句!以下述代碼爲例:

反例

private User saveUserInfo(String telephone) {
    return queryUser(telephone);

}

問題: 方法名爲 saveUser,其字面意思是:保存用戶信息,但其內部實現則爲查詢用戶信息,明顯的掛羊頭賣狗肉。

正例

private User getUser(String telephone) {
    return queryUser(telephone);

}

建議

  1. 定義接口時,接口返回對象,方法名,請求參數 三者語義要統一,避免上述問題產生。
  2. 方法名儘量能夠表達一個行爲,例如:get / remove / put 等,而不是用名詞來代替。

小結

上述幾種問題是最近 code review 過程中出現最多的幾類問題以及解決辦法。還有一些諸如 code style 類的問題,沒有在本文中描述,不過還是建議大家一定要抽時間看看《Java手冊》,每次看都有新的收穫!還有就是一些提升代碼質量的途徑,諸如:

  1. 回看一週前,一月前自己寫的代碼,對不滿意的代碼進行重構。
  2. 參加開源項目,給開源項目提交 Pull Request,提交的代碼會有多人進行code review,直至滿足規範後,纔會進行 merge。

 

相關閱讀:

Seata 之 config 模塊源碼解讀

接口設計的五點建議 !

Seata 分佈式事務框架

Seata 之 rm-datasource 源碼解讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章