编写优美的代码之减少嵌套

编写优美的代码之减少嵌套

新年上班第一天~大家新年好!

要编写优雅的代码需要相当的功力,但是让代码看起来貌美如花只需要掌握一些姿势就行了。先看看下面的代码有什么不对?

private void syncApkWhitelistStatus(boolean isInWhiteList, String apkDirPath, List<ExtCategory> cateList) {
        ExtCategory mApkItem;
        
        if (cateList != null && cateList.size() > 1) {
            mApkItem = cateList.get(0);
            if (mApkItem != null) {
                ArrayList<TrashInfo> apkList = mApkItem.trashInfoList;
                for (TrashInfo apkInfo : apkList) {
                    if (apkInfo.bundle.getString(TrashClearEnv.dirPath).startsWith(apkDirPath)) {
                        apkInfo.isInWhiteList = isInWhiteList;
                        if (apkInfo.isInWhiteList) {
                            apkInfo.isSelected = false;
                        } else {
                            apkInfo.isSelected = TrashClearEnv.CLEAR_TYPE_ONEKEY == apkInfo.clearType ? true : false;
                        }
                    }
                }
            }
        }
    }

看起来很对称是吧?美么?

If you need more than 3 levels of indentation, you’re screwed anyway, and should fix your program.

Linux之父Linus Torvalds说:如果你的代码里需要有超过三层的缩进,那么你已经搞砸了,应该修改你的代码。

我们今天就来谈谈,如何减少代码嵌套。

使用break,continue改变控制流程

if-else语句应该是编码中最常用的控制流程了,但是如果如果真的顺着思路一路if else下去,很有可能达到嵌套非常深的条件语句;这种代码你说它有问题吧,逻辑是对的也能work,但是它确实非常难看!

比如有如下代码:

private void test(boolean condition1, boolean condition2) {
        
        if (condition1) {
            while (true) {
                if (condition2) {
                    // 这里是很大一段代码,可以看到这一段相对你的函数有三层缩进
                }
            }
        }
    }

通过break关键字控制流程,可以达到如下效果:

private void test(boolean condition1, boolean condition2) {

        if (!condition1) {
            return;
        }

        while (true) {
            if (!condition2) {
                continue;
            }
            // 这里是你的核心逻辑; 只有一层嵌套
        }
    }

使用do-while(false)

do-while(false)结构可以达到goto的效果,灵活地使用它能有效地减少嵌套。但是,不推荐过多使用,特别是当他与一堆其他流程控制语句混在一起的时候,会导致代码可读性下降,得不偿失。

最常使用的case如下:

// goto case
{
  if(!a) goto done;
  //do something here
  if(!b) goto done;
  //do another thing here
  done:
   //final step goes here
}

// do ... while(0)
{
  do
  {
    if(!a) break;
    //do something here
    if(!b) break;
    //do another thing here   
  }while(false);
  //final step goes here
}

当然,do-while(false)还有很多奇技淫巧,感兴趣可以google,但是这种流程跳转不太好维护,就类似goto,在必要的时候使用有画龙点睛的效果。

使用jump table

如果复杂冗长的if-else语句只是为了做出分之选择的时候,可以用类似jump table的方式完成;我这里举个例子,原始代码如下:

@Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.button1) {
            startActivity(new Intent(this, StandardActivity.class));
        } else if (id == R.id.button2) {
            startActivity(new Intent(this, SingleTopActivity.class));
        } else if (id == R.id.button3) {
            startActivity(new Intent(this, SingleTaskActivity.class));
        } else if (id == R.id.button4) {
            startActivity(new Intent(this, SingleInstanceActivity.class));
        } else if (id == R.id.button4) {
            startActivity(new Intent(this, SingleInstanceActivity.class));
        } else if (id == R.id.button5) {
            startActivity(new Intent(this, SingleTopActivity.SingleTopActivity1.class));
        } else if (id == R.id.button6) {
            startActivity(new Intent(this, SingleTopActivity.SingleTopActivity2.class));
        } else if (id == R.id.button7) {
            startActivity(new Intent(this, SingleTopActivity.SingleTopActivity3.class));
        } else if (id == R.id.button8) {
            startActivity(new Intent(this, SingleTopActivity.SingleTopActivity4.class));
        } else if (id == R.id.button9) {
            startActivity(new Intent(this, SingleTopActivity.SingleTopActivity5.class));
        } else if (id == R.id.button10) {
            startActivity(new Intent(this, SingleTaskActivity.SingleTaskActivity1.class));
        } else if (id == R.id.button11) {
            startActivity(new Intent(this, SingleTaskActivity.SingleTaskActivity2.class));
        } else if (id == R.id.button12) {
            startActivity(new Intent(this, SingleTaskActivity.SingleTaskActivity3.class));
        } else if (id == R.id.button13) {
            startActivity(new Intent(this, SingleTaskActivity.SingleTaskActivity4.class));
        }
    }

这种代码我们可以建立一个table,然后让程序自动select,那么就简短多了:

SparseArray<Class<?>> sClassMap = new SparseArray<Class<?>>(){{
        put(R.id.button1, StandardActivity.class);
        put(R.id.button2, SingleTopActivity.class);
        put(R.id.button3, SingleTaskActivity.class);
        put(R.id.button4, SingleInstanceActivity.class);
        put(R.id.button5, SingleTopActivity.SingleTopActivity1.class);
        put(R.id.button6, SingleTopActivity.SingleTopActivity2.class);
        put(R.id.button7, SingleTopActivity.SingleTopActivity3.class);
        put(R.id.button8, SingleTopActivity.SingleTopActivity4.class);
        put(R.id.button9, SingleTopActivity.SingleTopActivity5.class);
        put(R.id.button10, SingleTaskActivity.SingleTaskActivity1.class);
        put(R.id.button11, SingleTaskActivity.SingleTaskActivity2.class);
        put(R.id.button12, SingleTaskActivity.SingleTaskActivity3.class);
        put(R.id.button13, SingleTaskActivity.SingleTaskActivity4.class);

    }};

    @Override
    public void onClick(View v) {
        int id = v.getId();
        startActivity(new Intent(this, sClassMap.get(id)));
    }

代码摘自DroidPlugin,改进的例子还使用了一种“双大括号初始化”的技术,可以简化集合Map的准备工作。我们可以把startActivity当作一个虚函数,在条件分之到达的时候,可以根据情况自动做出选择,这样就直接避免了分支语句的产生。

另外,这种技术可以一定程度上提升代码的执行效率。因为现代CPU构架都是流水线执行指令的,但是分支语句的存在阻碍了这种流水线的连续性(不同分支代码不同指令不同)因此有一种叫做“分支预测”的技术,使得流水线上尽量脸面不绝,这样CPU就不会闲着。但是一旦“分支预测”失败,就会收到“惩罚”浪费数十个时钟周期;如果用这种jump table的方式,就类似于条件求值指令,避免了分支语句,从而避免了这种有可能由于分支预测失败引起的性能下降。(微乎其微,了解即可~)

使用多态性重构代码

上面那种技术是一种面向过程的改进方式,如果程序比较复杂,可以用面向对象的多态性解决这个问题。为每一个if分支抽象出一个对象,在if分支上的操作看作是调用对象的方法;所有的这些if分支对象继承同一个接口,然后各自实现不同,这样,if语句就分解成了对一个多态对象的简单方法调用。

这种方法可以看作是上面jump table的加强版;如果只是为了控制流程,可以使用枚举类型而不是普通类型实现接口,这样表达的目的更加明确,代码也更优雅。另外,简单情况下就不要用这种办法了,杀鸡焉用牛刀。

使用异常

很多情况下我们使用条件语句是为了验证输入参数等的有效性,比如:

if (obj != null) {
    Some someThing = obj.getSomeThing();
    // do something else
    if (someThing != null) {
        someThing.call();
        // maybe else
        Some someThingElse = someThing.getSomeThingElse();
        if (someThingElse != null) {
            someThingElse.call();
        }
    }
}

这种频繁的判空代码我们称之为“样板代码”,它没有什么意义,但是又不得不做。如果你确定这部分代码一旦有一个为null整个调用就没有意义,何不什么判断都不做,直接手动捕获空指异常呢?

try {
    Some someThing = obj.getSomeThing();
    someThing.call();
    someThing.getSomeThingElse().call();
catch(NullPointerException e) {
    // deal with it
}

当然,使用try..catch,一旦异常触发,必然会降低程序的性能;如果做参数检验仅仅是为了避免极端情况的崩溃,在大部分情况下不会发生异常,那么可以放心使用;使用Java语言层面上的这种异常传递机制来处理嵌套也是一个不错的选择。

另外,由于Java里面goto语句没有效果,break用作goto只能在循环里面使用,合理使用异常也能达到goto的效果(比如深层嵌套返回)。

提取函数

如果嵌套过于复杂是由于业务本身的复杂性引起的,那么上述的优化办法作用就微乎其微了。这时候应该从程序结构上来保持代码的美观,把嵌套复杂的逻辑抽离出来,放在一个单独的函数里面去,然后取一个合适的函数名;这样既保持了主干核心逻辑代码的美观,也大大提高了程序的可读性。

最后,优化一下文章开头给出的代码,效果如下:

private void syncApkWhitelistStatus(boolean isInWhiteList, String apkDirPath, List<ExtCategory> cateList) {
        if (cateList == null || cateList.size() < 1) {
            return;
        }

        ExtCategory mApkItem = cateList.get(0);
        if (mApkItem == null) {
            return;
        }

        ArrayList<TrashInfo> apkList = mApkItem.trashInfoList;
        for (TrashInfo apkInfo : apkList) {
            if (!apkInfo.bundle.getString(TrashClearEnv.dirPath).startsWith(apkDirPath)) {
                continue;
            }
            apkInfo.isInWhiteList = isInWhiteList;
            apkInfo.isSelected = !apkInfo.isInWhiteList && (TrashClearEnv.CLEAR_TYPE_ONEKEY == apkInfo.clearType);
        }
    }

再看一下没改之前的样子:

private void syncApkWhitelistStatus(boolean isInWhiteList, String apkDirPath, List<ExtCategory> cateList) {
        ExtCategory mApkItem;
        
        if (cateList != null && cateList.size() > 0) {
            mApkItem = cateList.get(0);
            if (mApkItem != null) {
                ArrayList<TrashInfo> apkList = mApkItem.trashInfoList;
                for (TrashInfo apkInfo : apkList) {
                    if (apkInfo.bundle.getString(TrashClearEnv.dirPath).startsWith(apkDirPath)) {
                        apkInfo.isInWhiteList = isInWhiteList;
                        if (apkInfo.isInWhiteList) {
                            apkInfo.isSelected = false;
                        } else {
                            apkInfo.isSelected = TrashClearEnv.CLEAR_TYPE_ONEKEY == apkInfo.clearType ? true : false;
                        }
                    }
                }
            }
        }
    }

是不是清晰了许多!

文章被以下专栏收录