我在大廠做 CR——為什麼建議使用列舉來替換布林值

木宛城主發表於2024-10-16

使用列舉替換布林值主要基於以下幾個原因
● 可讀性
● 可擴充性
● 安全防控

可讀性

我們會定義 boolean 型別(truefalse)作為方法引數,雖然比較簡潔,但有時候引數的含義往往不夠清晰,造成閱讀上的障礙,

比如:引數可能表示“是否開啟某個功能”,但僅憑 truefalse 並不能一眼看出其真實意圖:

setDisable(false):到底是禁用還是啟用 --!
setInvalid(false):到底是無效還是有效 --!

相信我,這種“繞彎”的“雙重否定”表達方式,一定會耗費你不多的腦細胞一會兒:)
當然你可能會說:“不使用否定的名詞”,換成“直接表達”,setEnable(true),這一眼能識別是啟用,非常直觀;
是的,沒錯,但在我 10 餘年的程式設計生涯裡,相信我 setDisable(false) 遇到過無數次;
再舉個例子:
下面程式碼你能“一眼知道”引數 true 代表什麼含義嗎?

public static void main(String[] args) {
    convert("12.3456", 2, true);
}

/**
 * 將字串轉換成指定小數位數的值
 *
 * @param value
 * @param scale
 * @param enableHalfUp 是否需要四捨五入
 * @return
 */
public static String convertToValue(String value, int scale, boolean enableHalfUp) {
    if (enableHalfUp){
        //將字串"四捨五入"換成指定小數位數的值
    }else{
        //將字串"截斷"換到指定小數位數的值
    }
}

當然,現在 IDE 都有比較好的提示,但從“可讀性”角度,是不是隻能進入到方法定義看註釋去了解,甚至沒有註釋還得去翻程式碼去研究這個 boolean 到底是啥語義,引數再爆炸下,你能知道每個 boolean 型別引數代表什麼嗎?

convert("12.3456", 2, true,false,true,false,true);

這裡額外擴充套件一句,木宛哥搞過一段時間的 iOS 開發,如果是 Objective-C 語言,方法命名採用了較為直觀的格式,可以包含多個引數名稱“線性敘事”,以提高可讀性。這種情況,boolean 變數前往往有“名詞修飾”,會容易理解,如下所示:
[NSString stringWithCString:"something" enableASCIIStringEncoding:true]

再從 OC 語言回過來,對於這個問題,讓看看 JDK 是怎麼設計的

public static void main(String[] args) {
    BigDecimal value = new BigDecimal("12.34567");
    //四捨五入到兩位小數
    BigDecimal roundedValue = value.setScale(2, RoundingMode.HALF_UP);
    System.out.println(roundedValue);
}

看到了沒,BigDecimalsetScale 方法,透過定義列舉:RoundingMode 代表轉換規則,看到:RoundingMode.HALF_UP 一眼就知道要四捨五入,根本不需要看程式碼。
這樣增加了可讀性的,同時定義了列舉也支援更多擴充套件,如下馬上引入第二點好處:可擴充套件

可擴充套件性

如果未來需要增加更多狀態,使用 boolean 會受到擴充套件的限制

例如,如果當前有兩個狀態:enable(開)和 disable(關),而將來需要新增待機狀態,使用 boolean 就顯得不夠靈活。列舉則很容易擴充套件,能夠清晰地表示更多的狀態。
使用 boolean 表達功能狀態:

public void configureFeature(boolean enable) {
    if (enable) {
        // 開啟功能
    } else {
        // 關閉功能
    }
}

使用列舉表達功能狀態:

public enum FeatureMode {
    ENABLED,
    DISABLED,
    MAINTENANCE
}

public void configureFeature(FeatureMode mode) {
    switch (mode) {
        case ENABLED:
            // 開啟功能
            break;
        case DISABLED:
            // 關閉功能
            break;
        case MAINTENANCE:
            // 維護狀態
            break;
        default:
            throw new IllegalArgumentException("Unknown mode: " + mode);
    }
}

型別安全

錯誤的使用 Boolean 包裝類,有可能會引發空指標異常;

先拋一個問題:包裝類 Boolean 有幾種“值”?
Boolean 是包含兩個值的列舉:Boolean.TRUEBoolean.FALSE但別忘了,還可以是 null

一個真實的線上故障,Boolean 在某些情況下被錯誤地使用,可能會造成空指標異常
例假設你正在修改一個老舊系統的某個方法,這個方法返回 Boolean,有幾千行程式碼:

public static void main(String[] args) {
    if (checkIfMatched("Dummy")){
        System.out.println("matched");
    }
}

/**
 * 老舊系統裡一個異常複雜的方法,有幾千行
 * @param str
 * @return
 */
public static Boolean checkIfMatched(String str) {
    Boolean matched;
    //假設此處存在:複雜處理邏輯,暫時用dummy代替
    if ("Dummy".equals(str)) {
        matched = true;
    } else {
        matched = false;
    }
    return matched;
}

目前沒問題,但當功能不斷迭代後,複雜度也陡然上升,在某個特別的分支裡,沒有對 Boolean 賦值,至少在編譯時是不會報錯的:

public static void main(String[] args) {
    if (checkIfMatched("Dummy")) {
        System.out.println("matched");
    }
}

/**
   * 老舊系統裡一個異常複雜的方法,有幾千行
   *
   * @param str
   * @return
   */
public static Boolean checkIfMatched(String str) {
Boolean matched = null;
//假設此處存在:複雜處理邏輯,暫時用 dummy 代替
if ("Dummy".equals(str)) {
    //模擬:程式碼在演進的時候,有可能存在 matched 未賦值情況
    if (false) {
        matched = true;
    }
} else {
    matched = false;
}
return matched;
}

這個時候,危險悄然而至,還記得上面的問題嗎:
包裝類 Boolean 有幾種“值”?
現在 checkIfMatched() 方法在不同的情況下,方法會返回三個不同的值:true/false/null
這裡 null 是非常危險的,如果上游使用如下方式判斷條件,考慮下是否有問題?

if (checkIfMatched("Dummy")) {
    System.out.println("matched");
}

首先這裡不會編譯錯誤,但此處 if 條件處會自動拆箱,對於 null 值會得到 NullPointerException 異常;

小小總結

再回過頭看:“哪些場景建議使用列舉來替換布林值”,我認為要看功能點的易變程度去綜合評估:“越容易變化,越不能讓複雜度發散,越要由一處收斂,試想下一個 Boolean 的方法的變動是不是要評估所有上游的業務”;
所以並不是完全推翻布林值,木宛哥在此也只是丟擲一些程式碼的最佳化手段僅供參考。

相關文章