12

飞哥讲代码12:好代码应表意直白

 3 years ago
source link: http://lanlingzi.cn/post/technical/2020/0815_code/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

飞哥讲代码12:好代码应表意直白

2020-08-15

  |   技术  

  |  

2214 字 ~5分钟

下面代码都来源于部门某一中间件产品(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处理逻辑
    }

1.1 背后的知识

上述代码表面上的共性问题:

  • 代码层次嵌套比较深,容易产生bug;也不容易读懂代码,甚至隐藏问题。
  • 代码表达意图不够简洁,代码一写得比较绕,代码二存在废话,代码三存在break错误风险。

减少嵌套层次,降低代码复杂度是老生常谈的问题,也是大牛们经常提到的问题:

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

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

在C语言中,Linus的话应该是没有问题的,对于其它的语言要求过高。在我司的编程规范,也建议嵌套层次不要超过4层。

2 控制与逻辑分离

嵌套层次过深,表面上可能业务逻辑复杂,表现为控制流程太多过深;深层次可能的原因是我们思维方式引起的,正常的人思维肯定按顺序思考方式:先考虑正常情况,再考虑异常情况,依次递进地再思考。

我把嵌套过深分为四类,从代码现象来看,都是由于控制语句引起的复杂。

  • 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。减少嵌套层次,降低代码复杂度,值得你去追求。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK