什么是 “不嵌套主义者”#
不嵌套主义者绝不嵌套他们的代码!
好吧,尽可能不要
嵌套深度#
如果我们把每个左括号作为新一层嵌套的标志,下列代码块就是一个嵌套深度为 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 行是一个比较好的评判标准),为了可读性有必要抽取代码为子方法;
- 方法复用越多,参数越少,说明提炼的越好。
- 这在协作开发时尤为重要,除了首要保证功能的实现,其次就是好的可读性。