圈复杂度

前端代码质量 ConardLi

Posted by huangqing on November 11, 2019

定义

圈复杂度 (Cyclomatic complexity) 是一种代码复杂度的衡量标准,也称为条件复杂度或循环复杂度,它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。简称 CC 。其符号为 VG 或是 M 。

圈复杂度 在 1976 年由 Thomas J. McCabe, Sr. 提出。

衡量标准

代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。

圈复杂度 代码状况 可测性 维护成本
1 - 10 清晰、结构化
10 - 20 复杂
20 - 30 非常复杂
>30 不可读 不可测 非常高

计算方法

3.1 控制流程图

控制流程图,是一个过程或程序的抽象表现,是用在编译器中的一个抽象数据结构,由编译器在内部维护,代表了一个程序执行过程中会遍历到的所有路径。它用图的形式表示一个过程内所有基本块执行的可能流向, 也能反映一个过程的实时执行过程。

3.2 节点判定法

有一个简单的计算方法,圈复杂度实际上就是等于判定节点的数量再加上1。向上面提到的:if elseswitch casefor循环、三元运算符等等,都属于一个判定节点,例如下面的代码:

function testComplexity(*param*) {
    let result = 1;
    if (param > 0) {
        result--;
    }
    for (let i = 0; i < 10; i++) {
        result += Math.random();
    }
    switch (parseInt(result)) {
        case 1:
            result += 20;
            break;
        case 2:
            result += 30;
            break;
        default:
            result += 10;
            break;
    }
    return result > 20 ? result : result;
}

上面的代码中一共有一个if语句,一个for循环,两个case语句,一个三元运算符,所以代码复杂度为 4+1+1=6。另外,需要注意的是 ||&& 语句也会被算作一个判定节点,例如下面代码的代码复杂为3

function testComplexity(*param*) {
    let result = 1;
    if (param > 0 && param < 10) {
        result--;
    }
    return result;
}

3.3 点边计算法

M = E − N + 2P
  • E:控制流图中边的数量
  • N:控制流图中的节点数量
  • P:独立组件的数目

前两个,边和节点都是数据结构图中最基本的概念:

P代表图中独立组件的数目,独立组件是什么意思呢?来看看下面两个图,左侧为连通图,右侧为非连通图:

  • 连通图:对于图中任意两个顶点都是连通的

一个连通图即为图中的一个独立组件,所以左侧图中独立组件的数目为1,右侧则有两个独立组件。

对于我们的代码转化而来的控制流程图,正常情况下所有节点都应该是连通的,除非你在某些节点之前执行了 return,显然这样的代码是错误的。所以每个程序流程图的独立组件的数目都为1,所以上面的公式还可以简化为 M = E − N + 2

4. 降低代码的圈复杂度

我们可以通过一些代码重构手段来降低代码的圈复杂度。

重构需谨慎,示例代码仅仅代表一种思想,实际代码要远远比示例代码复杂的多。

4.1 抽象配置

通过抽象配置将复杂的逻辑判断进行简化。

4.2 单一职责 - 提炼函数

单一职责原则(SRP):每个类都应该有一个单一的功能,一个类应该只有一个发生变化的原因。

4.3 使用 break 和 return 代替控制标记

我们经常会使用一个控制标记来标示当前程序运行到某一状态,很多场景下,使用 break 和 return 可以代替这些标记并降低代码复杂度。

4.4 用函数取代参数

4.5 简化条件判断 - 逆向条件

某些复杂的条件判断可能逆向思考后会变的更简单

4.6 简化条件判断 -合并条件

将复杂冗余的条件判断进行合并。

4.7 简化条件判断 - 提取条件

将复杂难懂的条件进行语义化提取,提炼成函数。

5. 圈复杂度检测方法

5.1 eslint规则

eslint提供了检测代码圈复杂度的rules:

我们将开启 rules 中的 complexity 规则,并将圈复杂度大于 0 的代码的 rule severity 设置为 warnerror

    rules: {
        complexity: [
            'warn',
            { max: 0 }
        ]
    }

这样 eslint 就会自动检测出所有函数的代码复杂度,并输出一个类似下面的 message

Method 'testFunc' has a complexity of 12. Maximum allowed is 0
Async function has a complexity of 6. Maximum allowed is 0.
...

5.2 CLIEngine

构建及插件设计,查看原文前端代码质量-圈复杂度原理和实践,插件awesome-cli