圈复杂度概念
圈复杂度(Cyclomatic complexity,简写CC)也称为条件复杂度,是一种代码复杂度的衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度,其符号为VG或是M。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和 维护。程序的可能错误和高的圈复杂度有着很大关系。
圈复杂度计算方法
点边计算法
圈复杂度的计算方法很简单,计算公式为:
V(G) = E - N + 2
其中,e 代表在控制流图中的边的数量(对应代码中顺序结构的部分),n 代表在控制流图中的节点数量,包括起点和终点。
注意,1、所有终点只计算一次,即便有多个return或者throw;
2、节点对应代码中的分支语句
下面是典型的控制流程,如正常流程,if-else,switch-case,for/while:
代码示例-控制流图:
根据公式 V(G) = e – n + 2 = 12 – 8 + 2 = 6 ,上图的圈复杂段为6。
说明一下为什么n = 8,虽然图上的真正节点有12个,但是其中有5个节点为throw、return,这样的节点为end节点,只能记做一个。
节点判定法
其实,圈复杂度的计算还有更直观的方法,因为圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上1,也即控制流图的区域数,对应的计算公式为:
V (G) = P + 1
其中P为判定节点数,判定节点举例:
- if语句
- while语句
- for语句
- case语句
- catch语句
- and和or布尔操作
- ?:三元运算符
对于多分支的CASE结构或IF-ELSEIF-ELSE结构,统计判定节点的个数时需要特别注意一点,要求必须统计全部实际的判定节点数,也即每个ELSEIF语句,以及每个CASE语句,都应该算为一个判定节点。
判定节点在模块的控制流图中很容易被识别出来,所以,针对程序的控制流图计算圈复杂度V(G)时,一般采用点边计算法,也即V(G)=e-n+2;而针对模块的控制流图时,可以直接使用统计判定节点数,这样更为简单。
圈复杂度计算练习
练习1:
1void sort(int * A)
2{
3 int i=0;
4 int n=4;
5 int j = 0;
6 while(i < n-1)
7 {
8 j = i +1
9 while(j < n)
10 {
11 if (A[i] < A[j])
12 swap(A[i], A[j]);
13 }
14 i = i + 1
15 }
16}
其圈复杂度为:V(G) = 9 - 7 + 2 = 4
练习2:
1U32 find (string match){
2 for(auto var : list)
3 {
4 if(var == match && from != INVALID_U32) return INVALID_U32;
5 }
6 //match step1
7 if(session == getName() && key == getKey())
8 {
9 for (auto& kv : Map)
10 {
11 if (kv.second == last && match == kv.first)
12 {
13 return last;
14 }
15 }
16
17 }
18 //match step2
19 auto var = Map.find(match);
20 if(var != Map.end()&& (from != var->second)) return var->second;
21
22 //match step3
23 for(auto var: Map)
24 {
25 if((var.first, match) && from != var.second)
26 {
27 return var.second;
28 }
29 }
30 return INVALID_U32;
31 };
其圈复杂度为:V(G) = 1(for) + 2(if) + 2(if) + 1(for) + 2(if) + 2(if) + 1(for) + 2(if) + 1= 14
圈复杂度的意义
在缺陷成为缺陷之前捕获它们
圈复杂度和代码质量
McCabe&Associates 公司建议尽可能使 V(G) <= 10。NIST(国家标准技术研究所)认为在一些特定情形下,模块圈复杂度上限放宽到 15 会比较合适。
因此圈复杂度 V(G)与代码质量的关系如下:
圈复杂度与缺陷
许多研究指出一模块及方法的圈复杂度和其中的缺陷个数有相关性:圈复杂度最高的模块及方法,其中的缺陷个数也最多。
另外也方便做测试计划,确定测试重点。对于圈复杂度最高的模块及方法,测试时做重点测试。
圈复杂度与结构化测试
此外,它还为测试设计提供很好的参考。一个好的用例设计经验是:创建数量与被测代码圈复杂度值相等的测试用例,以此提升用例对代码的分支覆盖率。
圈复杂度与TDD
TDD(测试驱动的开发,test-driven development)和低CC值之间存在着紧密联系。在编写测试时,开发人员会考虑代码的可测试性,倾向于编写简单的代码,因为复杂的代码难以测试。因此TDD的“代码、测试、代码、测试” 循环将导致频繁重构,驱使非复杂代码的开发。
圈复杂度与遗留代码
对于遗留代码的维护或重构,测量圈复杂度特别有价值。一般使用圈复杂度作为提升代码质量的切入点。
圈复杂度与CI
在持续集成环境中,可以基于时间变化维度来评估模块或函数的复杂度和增长值。如果CC值在不断增长,那么应该开展两项活动:
1、确保相关测试的有效性,减少故障风险。
2、评估重构必要性和具体方式,以降低出现代码维护问题的可能性。
如何降低圈复杂度
分两个方向降低圈复杂度,一是拆分函数,二是尽量减少if、else、while、case等这些流程控制语句。
拆分函数
圈复杂度的计算范围是在一个function内的,将业务代码拆分成一个一个的职责单一的小函数,如此除了能够降低圈复杂度,也能提高代码的可读性和可维护性。
减少流程控制
1. 减少不必要条件、循环分支,尽量少用 if …else … ,采用三元表达式替换 if else;
if (DMAINFO_ABNORMAL(wDmaSts))
{
bRetryType = RD_ERR_DATA_ERROR; /*软解Fail*/
}
else
{
bRetryType = RD_ERR_SOFT_PASS; /*软解Pass*/
}
//修改后:
bRetryType = (DMAINFO_ABNORMAL(wDmaSts)) ? RD_ERR_DATA_ERROR : RD_ERR_SOFT_PASS;
2. 合并条件表达式,比如使用 a || b || c;
3. 去掉没有必要的else
if (false)
{
return;
}
else
{
c = a;
}
//修改后:
if (false)
{
return;
}
c = a;
4. 同一条件多处出现,重构函数
if (b)
{
if (a)
{
Func1();
}
}
else
{
if (a)
{
Func2();
}
}
if (a)
{
Func3();
}
//修改后:
if (a)
{
if (b)
{
Func1();
}
else
{
Func2();
}
Func3();
}
5.提炼函数
......
圈复杂度工具
圈复杂度的工具有很多,大致有三类: