背景
代码的可测试性和可维护性是非常重要的,比如,下面的代码:
只是输出简单的"Hello, world", 大家都不会否认这个代码写得太复杂太难维护了。那么,有没有什么度量指标来度量什么样的代码是简单的,可维护的代码呢?
圈复杂度
度量代码复杂度的指标有很多,比如LOC/SLOC/圈复杂度/认知复杂度等等,圈复杂度就是用来度量代码的可维护性,可测试性的,它的使用范围最广。为什么需要圈复杂度呢?
首先,我们介绍一下代码测试覆盖率中的常见的几个概念:
- 行覆盖率 (Line coverage): 测试覆盖了代码行的比率
- 函数覆盖率(Function coverage): 测试覆盖到了代码中函数的比例,粒度较大
- 分支覆盖率(Branches coverage): 测试覆盖到了代码的if/else等执行路径的比例
比较复杂的是分支覆盖率,分支覆盖率指的是测试覆盖跳转分支的比例,程序的跳转分支是是一个非常底层的概念,比如
int foo(int num)
{
int a = 1;
if (num == 2) {
a = 3;
}
if (num == 3) {
a = 5;
}
if (num == 5) {
a = 7;
}
if (num == 7) {
a = 2;
}
return a;
}
我们可以在这里查看到跳转分支的总数是12个, 如果需要做到将跳转都测试到,需要的测试用例个数是就等同与改函数的圈复杂度,也就是5.
比如下面的例子:
在调用foo(true)时代码运行正常,且有100% code coverage, 但是调用foo(false)时,程序就回crash。
圈复杂度的值实际上对应于如果需要测试到某个函数的所有代码分支,需要的最少的测试用例的个数。上面的例子明显就是测试不充分的表现,代码的圈复杂度是2,我们的测试用例数却是1,flag为false时,这个分支没有测试到。
这里补充说明一下,
再看另外一个例子:
它的控制流程图(CFG: control-flow-graph)如下图1:
2和5处是两个if,我们只需要执行路径如下的3个测试用例:
Case 1: (1, 2, 3, 5, 6, 8)
Case 2: (1, 2, 4, 5, 6, 8)
Case 3: (1, 2, 3, 5, 7, 8)
就可以cover到所有的代码分支。
当然测试了所有的代码分支并不是测试了代码的所有执行路径,因为测试代码所有执行路径基本上是Mission impossible, 比如一个含有25个If/else的函数,要把所有可能的执行路径进行测试,需要的用例个数是2^25=3300万个测试用例。
计算圈复杂度
圈复杂度的计算有两种方法:
-
一种是根据程序的CFG图,采用边点法计算,公式如下:
V(G) = E - N + 2
Where,
E = Number of edges
N = Number of nodes如图1中E=9, N = 8, 那么V(G) = 9 - 8 + 2 = 3
-
另外一种是计算程序中的if/else if/for/while/&&/||/switch case等的数目:
V(G) = P + 1
Where,
P = Number of decision nodes (node that contains condition)如图1中含有两个if, P = 2, 那么V(G) = 2 + 1 = 3
圈复杂度值推荐值
虽然我们可以通过上面的公式计算出来圈复杂度,但是具体采用什么样的圈复杂度值作为代码好坏的评价呢?
- MISRA在它们的报告的第38页中推荐采用15作为阈值
- 一些大公司,比如华为等,采用15作为代码合入检查阈值
圈复杂度检测工具
Lizard是一个开源的,支持多语言的圈复杂度检查工具,它具有以下优点:
- MIT License
- 维护活跃积极
- 多语言支持
- 开箱即用,也可嵌入式使用
降低圈复杂度的方法
降低圈复杂度主要有一下几种方法:
- 提取函数,以为圈复杂度主要计算的是单个函数的复杂度,一个函数调用另外一个函数并不会增加这个函数本身的圈复杂度,所以将函数中某个功能单独抽取出去可以降低圈复杂度。
- 采用表驱动法减少if分支判断
- 避免手写for循环: 下面删除vector中某个元素的例子,可以由最初的圈复杂度4降为2:
认知复杂度
当然,圈复杂度也有它的缺点:圈复杂度将if/for/while等条件语句等同的代入计算圈复杂度值,而不考虑它们的嵌套深度。这样的后果是,对于具有同样圈复杂度的不同函数,我们没法区分这是一个逻辑简单的功能函数还是一个逻辑复杂的功能函数,比如下面两个函数,圈复杂度都是5,但是显然右边的逻辑更复杂,更难维护一些:
基于这个原因,SonarQube引入了认知复杂度(Cognitive Complexity)的概念。可以说,认知复杂度是一种考虑了嵌套深度的,更严格的圈复杂度。下图中对于同一个函数,其圈复杂度是5,认知复杂度是8。
目前SonarQube上同时支持认知复杂度和圈复杂度的检查,当然,如果降低了圈复杂度,一定也同时降低了认知复杂度。
Reference: