详解圈复杂度

圈复杂度概念

圈复杂度(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.提炼函数

......

圈复杂度工具

圈复杂度的工具有很多,大致有三类:

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值