衡量代码的好坏,有很多标准,比如 bug 率,时间复杂度,理解的难易程度等等。 bug 与时间复杂度的多少很容易衡量,唯独「理解的难易程度」没有一套大家都知道而且通用的测量方法。

闲逛时发现一个用来测量代码复杂度的 vscode 插件,它的理论基础是 Cyclomatic Complexity,以下翻译成循环复杂度

循环复杂度理论有几条数学公式,应用起来却很简单:

  • 每一个方法的起始复杂度为 1
  • 按顺序从上到下看方法内部可执行的代码。只要遇到一个 判断操作 就增加 1 复杂度
  • 判断操作包括 if/while/&&/||/for/case/ternary operator 等等

本文的重点不在循环复杂度,想深入了解的话请看这篇文章

我比较关心的是,为什么它在前端界不流行。理由应该不是大家不关心代码的好坏,而是这种衡量方式存在问题。

code complexity javascript 之类的关键词搜了下,发现 eslint 早就可以配置 complexity 规则。

作为一个坚持己见的人,接受不了这个——它早就流行开去的——事实,我点开 eslint complexity 页面中每一条参考链接,最终看到这篇对循环复杂度提出异议的文章

作者从多个角度评价这套理论:

  • 复杂度相同的两段代码,理解难易程度可以相差很远;因为有多少个判断分支就会相应增加多少复杂度,循环复杂度衡量的是“代码有多难测试”,而不是“代码有多难读懂”;
  • 循环复杂度于 1976 年面世,当时的编程语言很简单,今天的 Javascript 远远比那时循环复杂度的应用对象——Fortran——复杂得多,比如异步编程、闭包等等;
  • 同样是判断逻辑,嵌套的判断语句,比不嵌套的判断语句要更难懂。两者不应该拥有相同的复杂度;
  • 循环复杂度没有测量出代码的内聚程度,也没测量出代码在多大程度上遵循得墨忒耳定律
  • 循环复杂度没有考虑递归调用。递归调用应该增加复杂度;
  • 循环复杂度没有考虑 try…catch 语句, catch 是一种特殊类型的 if (如果出现异常,就执行以下代码);
  • 循环复杂度只关心代码结构复杂度,没有测量数据复杂度;

上面几点都很有道理,我认为这才是循环复杂度不流行的原因。

接下来介绍另一种测量代码复杂度的理论:Cognitive Complexity,以下翻译成认知复杂度,它解决了上面七个问题中的四个。

认知复杂度并不复杂,它只有三个基本原则:

  • 使用编程语言自带的语法糖(shorthand)并不会增加复杂度;
  • 只要打断程序的线性执行流就会增加复杂度,比如 条件判断、循环、跳转、异常捕获、递归调用;
  • 在嵌套上下文里打断程序的线性执行流,会带来更多的复杂度。

第一条原则可以用 ecmascript 将会推出的 optional chaining operator 语法来解释。使用传统的方式,以下代码复杂度是 3 :

var person;
if(person && person.profile && person.profile.name) {
  console.log(person.profile.name)
}

如果使用 optional chaining operator ,代码复杂度是 1 :

var person;
if(person?.profile?.name) {
  console.log(person.profile.name)
}

这条原则将语法糖当成是一个——隐藏了具体的实现方式的——方法调用。

第二条原则跟循环复杂度类似,在条件判断之余,还考虑了跳转、异常捕获和递归。

第三条原则考虑了嵌套对复杂度的影响。在越深的嵌套上下文里打断程序线性执行流,增加越多的复杂度。这很符合直觉:嵌套层级越深,代码越难懂。

这个文档能看到上面三条原则映射到实际代码上的具体实现。

参考文章最开始提到的插件源码以及这份文档,不难实现一个测量认知复杂度的工具(我做出来了,因为可能会违反公司政策,暂时不放源码出来)。

在对自己的项目用工具分析过后,发现分析认知复杂度至少有两点积极意义:

  • 可以衡量项目的业务复杂度,在接手别人的项目前看看业务是不是很复杂,坑不坑
  • 可以看到某个方法的写法是不是有问题,加以改良。如下:
    // 改良之前,复杂度: 5
    if(sex === 'female') { // +1
      if(age < 18) { // +2 这个 if 处于嵌套上下文中,会增加额外的复杂度
        return age
      } else { // +1
        return 18
      }
    } else { // +1
      return age
    }
    
    // 改良之后,复杂度:2
    if(sex === 'female' // +1
      && age > 18) { // +1
        return 18 
    }
    return age
    

有时间再做一个基于认知复杂度理论的分析代码复杂度的 vscode 插件,一定很有意思。

参考