BlackFlame33

BlackFlame33

若无力驾驭,自由便是负担。个人博客 https://blackflame33.cn/

為何要成為「不嵌套主義者」

什麼是 “不嵌套主義者”#

不嵌套主義者絕不嵌套他們的代碼!
好吧,盡可能不要

嵌套深度#

如果我們把每個左括號作為新一層嵌套的標誌,下列代碼塊就是一個嵌套深度為 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 以下。

消除嵌套的兩種方法#

  1. 提煉。即把方法中處理同一件事的代碼塊抽取成子方法。

  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的重複賦值,而且對DoubleFloat的處理邏輯是一致的,哦,重複代碼的警笛響了!

以下是重構後的代碼(這裡使用了 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 行是一個比較好的評判標準),為了可讀性有必要抽取代碼為子方法;
  • 方法重用越多,參數越少,說明提煉的越好。
  • 這在協作開發時尤為重要,除了首要保證功能的實現,其次就是好的可讀性。
相關鏈接:

代碼之美

Java 的模式匹配

🌟工作一年,我重新理解了《重構》

如何有效的解決代碼的圈複雜度

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。