案例
下面代码都来源于部门某一中间件产品(java)的源码,代码风格(此风格非格式风格而是逻辑思维风格)并且在整个源码中具有普遍性。
代码一:
public void run() {
while(!(this.stopped.get())) {
try {
synchronized (this.lock) {
while (this.endTime - System.currentTimeMills() > 0L) {
try {
if (this.stopped.get()) {
return;
}
this.lock.wait(this.endTime - System.currentTimeMillis());
// 省略主要业务逻辑
} catch (IntrruptedExecption e) {
// 省略日志打印
}
} // 注意:这个while之后并没有其它的逻辑
}
} catch(Throwable e) {
// 省略日志打印
}
}
}
代码意思是可能在等待的最大时间内,中间可以被通知执行主逻辑,然后再进入等待下次通知。在易读性上的问题:
- 双重while, 双重try/catch,增加代码嵌套层次,代码有六层,由于跨度较大,掩盖了要表达的业务逻辑,不容易看懂。
- 第一个while判断是
否定之否定
判断,不够直接,stoppped不需采AtomicBoolean,使用volatile变量即可。 - 代码有bug(嵌套太深隐藏了bug),当超过最大时间时,若没有设置stopped标识位,空循环占CPU。
建议优化:
- 由于synchronized是对lock的wait方法同步,wait后面的逻辑并不需要再同步保护,不应该锁整个while,减少锁的粒度。可以对wait逻辑单独抽取一个方法,直白表示是要waitNotify。
- 去掉
否定之否定
。把AtomicBoolean stopped变成volatile boolean running,判断更直白,running表示还得继续。
public void run () {
while(this.running) {
try {
if ( waitNotify() ) {
// 省略主要业务逻辑
}
} catch(Throwable e) {
// 省略日志打印
}
}
}
// 新增方法,表意是要等通知
private boolean waitNotify() {
if (this.endTime - System.currentTimeMills() <= 0L) {
this.running = false; // 超时,避免空循环
return false;
}
if (!this.running) {
return false;
}
try {
synchronized (this.lock) {
this.lock.wait(this.endTime - System.currentTimeMillis());
}
return true;
} catch (IntrruptedExecption e) {
// 省略日志打印
return false;
}
}
代码二:
public boolean deleteFile(File file)
if (file.isFile() && file.exists()) {
bool flag = file.delete()
if (flag) {
return true;
} else {
return false;
}
} else {
return false;
}
}
上面的代码的问题比较明显:
- 存在废话
建议优化:
public boolean deleteFile(File file)
if (file.isFile() && file.exists()) {
return file.delete()
}
return false; // 其实这个false与上面file.delete()返回false含义不一样,本文不深究这个问题
}
代码三:
switch(getMessageResult().getStatus()) {
case FOUND: {
while (iter.hasNext()) {
SelectMapedBufferResult selectMapedBuffer = iter.next();
try {
// 省略其它业务逻辑
} finally {
selectMapedBuffer.release();
}
}
messageList.addAll(mqMessageList);
break;
}
case OFFSET_TOO_SMALL:
// 省略其它的CASE处理逻辑
}
上面的代码问题主要是:
- switch中嵌套while,若while循环中一个break,它到底是break while还是break switch,不一小心就是产生一个bug。
解决上述问题也比较简单:
- 对于switch分支,每个分支的处理逻辑应该提取函数。
建议优化:
switch(getMessageResult().getStatus()) {
case FOUND:
processFound(...)
break;
case OFFSET_TOO_SMALL:
// 省略其它的CASE处理逻辑
}
背后的知识
上述代码表面上的共性问题:
- 代码层次嵌套比较深,容易产生bug;也不容易读懂代码,甚至隐藏问题。
- 代码表达意图不够简洁,代码一写得比较绕,代码二存在废话,代码三存在break错误风险。
减少嵌套层次,降低代码复杂度是老生常谈的问题,也是大牛们经常提到的问题:
If you need more than 3 levels of indentation, you’re screwed anyway, and should fix your program.
Linus曾经说过:如果你的代码里需要有超过三层的缩进,那么你已经搞砸了,应该修改你的代码。
在C语言中,Linus的话应该是没有问题的,对于其它的语言要求过高。在我司的编程规范,也建议嵌套层次不要超过4层。
控制与逻辑分离
嵌套层次过深,表面上可能业务逻辑复杂,表现为控制流程太多过深;深层次可能的原因是我们思维方式引起的,正常的人思维肯定按顺序思考方式:先考虑正常情况,再考虑异常情况,依次递进地再思考。
我把嵌套过深分为四类,从代码现象来看,都是由于控制语句引起的复杂。
- if/else引嵌套。
- for/while嵌套。
- try/catch嵌套。
- 上述的组合嵌套。
总结的好处是可以给我带来新的思考,我们写代码的思维逻辑可不可以不是顺序的,而是站在更高一层来看,总体原则是否可能把控制与逻辑分离:
- 控制: 即上述说的各个控制语名,它只是控制你的主要逻辑的走向。
- 逻辑: 才是程度真实要执行的代码,是业务主要完成的功能。
如果我们能把 控制
与 逻辑
部分有效的分开,那么代码将会变得更加容易看懂、维护和改进。
对于 控制
的优化,则方法要根据不同控制语句而论:
if/else 的优化
- 使用卫语句,异常条件优先返回
if () {
if () {
if () {
// ...
}
} else if() {
}
}
可优化为:
if (!...) {
return ;
}
if (...) {
return ;
}
if (...) {
....
}
- 把嵌套的 if 换成 if {} else if {} 语句
- 将 if 换成 case 语句
- 优先使用&&,在可以使用&&条件判断的地方要避免使用连续嵌套的if
- 将深层嵌套抽出来放到子函数
for/while优化
- 采用使用break,continue改变控制流程
for(int a : list) {
if (...) {
//
}
}
优化为:
for(int a : list) {
if (!...) { // 反着写
continue;
}
}
Switch优化
- 不要根据类型标签进行分支,而要优先使用多态函数。
- 采用表驱动查找,每个类型标签分支对应一个。
try/catch优化:
- 嵌套的try/catch表明你没有干净地编码,说明他们不在一个层次,肯定要把内部的try/catch的逻辑提取函数了。
对于 逻辑
的分开,最直接办法就是提取函数:
- 复杂的if/else内的逻辑提取函数
- for/while循环体内的逻辑提取函数
- switch的分支处理逻辑提到函数
更复杂的 逻辑
分离,则需要采用一些设计手段,如状态机模式与策略模式的核心思维都是把逻辑分散化,不同的处理逻辑分布到各个子类中。
结语
干净的代码首先肯定是易读的代码,直接白了的对话大家都喜欢,同样代码不应该隐藏其所表达的意图,而是表意直白,不需要让读者太多的思考。代码嵌套非常影响易读性,还会带来退出分支过多,需要人思考的方面也就越多,稍不留神就会搞出大Bug。减少嵌套层次,降低代码复杂度,值得你去追求。