什麼是 “不嵌套主義者”#
不嵌套主義者絕不嵌套他們的代碼!
好吧,儘可能不要
嵌套深度#
如果我們把每個左括號作為新一層嵌套的標誌,下列代碼塊就是一個嵌套深度為 4 的方法:
public int calculate(int bottom, int top)
{ // 1 😎
if (top > bottom)
{ // 2 🤨
int sum = 0;
for (int number = bottom; number <= top; number++)
{ // 3 🤔
if (number % 2 == 0)
{ // 4 😡
sum += number;
}
}
return sum;
}
else
{
return 0;
}
}
這個方法還算邏輯簡單的。嵌套深度大起來,會顯著影響閱讀代碼和理清邏輯。我們需要儘可能將嵌套深度控制在 2 以下。
消除嵌套的兩種方法#
-
提煉。即把方法中處理同一件事的代碼塊抽取成子方法。
-
反轉。即通過反轉
if-else
語句,使方法儘早return
。
1. 提煉#
讓我們使用提煉把上述代碼的結構優化一下:
int filterNumber(int number)
{
if (number % 2 == 0)
{
return number;
}
return 0;
}
public int calculate(int bottom, int top)
{ // 1
if (top > bottom)
{ // 2
int sum = 0;
for (int number = bottom; number <= top; number++)
{ // 3
sum += filterNumber(number);
}
return sum;
}
else
{
return 0;
}
}
看起來並沒有好太多。。。但至少現在我能一眼看出for
循環內部是讓篩選過的數字進行累加。
2. 反轉#
接下來我們使用反轉。當你把 “正面” 條件的分支放在更深的地方時,代碼就會嵌套很多層。若我們把 “負面” 條件的分支放到前面,就可以使程序儘早返回,同時不必嵌套else
部分的代碼。
讓我們試試看:
int filterNumber(int number)
{
if (number % 2 == 0)
{
return number;
}
return 0;
}
public int calculate(int bottom, int top)
{ // 1
if (top <= bottom)
{ // 2
return 0;
}
// 反轉後,既然程序能運行到這裡,說明 top > bottom,那麼就可以少一層縮進
int sum = 0;
for (int number = bottom; number <= top; number++)
{
sum += filterNumber(number);
}
return sum;
}
當我們有多處條件檢驗時,通過不斷反轉條件,儘量讓 “負面” 條件提前處理,儘早返回。如此,我們就會形成一種 “驗證守護”。就好像,事先聲明方法的要求,符合要求後,再執行實現方法功能的核心部分。
這樣,“正面” 條件的代碼都在下面,而 “負面” 條件的代碼,它們都進行了縮進。
我們在閱讀方法的核心部分時,也不用再記當前方法的狀態。
練手#
讓我們看看下面這段 “傑作”:
private static String getValueText(Object value) {
final String newExpression;
if (value instanceof String) {
final String string = (String)value;
newExpression = '"' + StringUtil.escapeStringCharacters(string) + '"';
}
else if (value instanceof Character) {
newExpression = '\'' + StringUtil.escapeStringCharacters(value.toString()) + '\'';
}
else if (value instanceof Long) {
newExpression = value.toString() + 'L';
}
else if (value instanceof Double) {
final double v = (Double)value;
if (Double.isNaN(v)) {
newExpression = "java.lang.Double.NaN";
}
else if (Double.isInfinite(v)) {
if (v > 0.0) {
newExpression = "java.lang.Double.POSITIVE_INFINITY";
}
else {
newExpression = "java.lang.Double.NEGATIVE_INFINITY";
}
}
else {
newExpression = Double.toString(v);
}
}
else if (value instanceof Float) {
final float v = (Float) value;
if (Float.isNaN(v)) {
newExpression = "java.lang.Float.NaN";
}
else if (Float.isInfinite(v)) {
if (v > 0.0F) {
newExpression = "java.lang.Float.POSITIVE_INFINITY";
}
else {
newExpression = "java.lang.Float.NEGATIVE_INFINITY";
}
}
else {
newExpression = Float.toString(v) + 'f';
}
}
else if (value == null) {
newExpression = "null";
}
else {
newExpression = String.valueOf(value);
}
return newExpression;
}
我直接頭皮發麻,但其實這段代碼雖然看起來複雜,但邏輯還是清晰的,只是。。。所有的處理邏輯都在一個方法裡,看起來很 “唬人”。
觀察到原代碼中對value
的重複判斷以及newWxpression
的重複賦值,而且對Double
和Float
的處理邏輯是一致的,哦,重複代碼的警笛響了!
以下是重構後的代碼(這裡使用了 jdk 新版本的特性,switch 可以返回值):
private static String getValueText(Object value) {
final String newExpression = switch (value) {
case String string -> '"' + StringUtil.escapeStringCharacters(string) + '"';
case Character character -> '\'' + StringUtil.escapeStringCharacters(value.toString()) + '\'';
case Long aLong -> value.toString() + 'L';
case Double aDouble -> getNewExpression(aDouble);
case Float aFloat -> getNewExpression(aFloat);
case null -> "null";
default -> String.valueOf(value);
};
return newExpression;
}
// getNewExpression() 的具體實現
重構後的代碼更簡潔,可讀性也更高了。當然示例代碼比起if-else
更適合使用switch
處理。如果針對if-else
類型,還可以使用反轉。
最後#
可以理解為如果細節展現的過多,那麼代碼複雜度就會高,可讀性也就會差;
- 嵌套深度儘可能維持在 2 以下,如果超過 2,就要考慮重構代碼;
- 有兩種方法可以減少嵌套深度:提煉與反轉。反轉讓 “負面” 條件的代碼儘早返回,使得方法的核心部分都在下面;提煉方便我們理清代碼邏輯,又不必知道細節;
- 見名知意的方法名很重要;
- 一個方法最好只做一件事,如果方法過長(50 行是一個比較好的評判標準),為了可讀性有必要抽取代碼為子方法;
- 方法復用越多,參數越少,說明提煉的越好。
- 這在協作開發時尤為重要,除了首要保證功能的實現,其次就是好的可讀性。