使用列舉替換布林值主要基於以下幾個原因
● 可讀性
● 可擴充性
● 安全防控
可讀性
我們會定義
boolean
型別(true
或false
)作為方法引數,雖然比較簡潔,但有時候引數的含義往往不夠清晰,造成閱讀上的障礙,
比如:引數可能表示“是否開啟某個功能”,但僅憑 true
和 false
並不能一眼看出其真實意圖:
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);
}
看到了沒,BigDecimal
的 setScale
方法,透過定義列舉: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.TRUE
和 Boolean.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
的方法的變動是不是要評估所有上游的業務”;
所以並不是完全推翻布林值,木宛哥在此也只是丟擲一些程式碼的最佳化手段僅供參考。