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 的模式匹配

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

如何有效的解决代码的圈复杂度

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。