引言
代码复杂度低,代码的质量不一定好,但如果代码的复杂度很高,则代码质量一定不好。软件程序中的圈复杂度代表了程序代码的复杂程度,它与软件的缺陷个数有高度的正相关。圈复杂度越高的程序代码,其缺陷个数也可能越多。一般来说,圈复杂度大于10的程序方法存在很大的出错风险。
翻阅《软件测试基础教程》,在静态测试技术章节介绍到了圈复杂度,感觉写的内容比较少,只有1页多。近些年,随着白盒测试和TDD越来越多的应用,圈复杂度作为软件测试分析和设计的一个重要依据,应该得到更多的关注。因此,想再多补充介绍一些关于圈复杂度的内容,包括圈复杂度的测试工具等。
什么是圈复杂度?
圈复杂度,Cyclomatic complexity,简写CC,也称为条件复杂度,是软件程序代码复杂度的衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂程度,其符 为V(G)或是M。
在软件测试的概念里,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的独立路径条数,也可理解为覆盖所有的可能情况需使用的最少测试用例数,即合理的预防错误所需测试的最少路径条数。因为圈复杂度通过某一方法来表示路径,这是用来确定某一方法到达 100% 的覆盖率将需要多少测试用例的一个好方法。
圈复杂度大,说明程序代码的判断逻辑复杂,可能会导致软件的质量低,且难于测试和维护。根据经验,程序的可能错误和圈复杂度高有着很大关系。
控制流程图是一个过程或程序的抽象表现,是用在编译器中的一个抽象数据结构,由编译器在内部维护,代表了一个程序执行过程中会遍历到的所有路径。它用图的形式表示一个过程内所有基本块执行的可能流向, 也能反映一个过程的实时执行过程。
下面是一些常见的控制流程(表示程序执行流程的有向图)
圈复杂度主要与控制流图中的分支语句(if、else、switch 等)的个数成正相关。
当一段代码中含有较多的分支语句,其逻辑复杂程度就会增加。
在计算圈复杂度时,可以通过程序控制流图方便的计算出来。
如果一段源码中不包含控制流语句(条件或决策点),那么这段代码的圈复杂度为1,因为这段代码中只会有一条路径。
如果一段代码中仅包含一个if语句,且if语句仅有一个条件,那么这段代码的圈复杂度为2。
如果代码中包含两个嵌套的if语句,或是一个if语句有两个条件,那么这个代码块的圈复杂度为3。
圈复杂度的计算方法
圈复杂度的计算方法常用的有三种,都比较很简单,如下。
方法一,点边计算法,计算公式为:
V(G) = E – N + 2
其中,E表示控制流图中边(Edge)的数量,N表示控制流图中节点(Node)的数量。
例如:
V(G)=E-N+2=10边-7点+2=5
方法二:节点判定法,计算公式为:
V(G) =判定节点数+1
判定节点指包括条件的节点。
这是更直观的方法,因为圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上1,也即控制流图的区域数。
例如,
上图中,节点1、2、3、4为判定节点
故,V(G)=4+1=5
判定节点举例:
if语句
while语句
for语句
case语句
catch语句
and和or布尔操作
注意:对于多分支的CASE结构,或IF-ELSE IF-ELSE结构,统计判定节点的个数时需要特别注意一点,要求必须统计全部实际的判定节点数,也即每个ELSE IF语句,以及每个CASE语句,都应该算为一个判定节点。
方法三:区域数法,计算公式为:
V(G)=R
其中R代表平面被控制流图划分成的区域数。
例如,
上图中被控制流图划分成的区域数有5块
故 ,V(G)=R=5
圈复杂度的计算方法在具体应用时,应针对不同的对象,采用适宜的方法。
针对程序的控制流图,计算圈复杂度V(G)时,最好采用第一个公式,也即V(G)=E-N+2。
而针对模块的控制流图时,判定节点在模块的控制流图中很容易被识别出来,所以,可以直接统计判定节点数。
针对复杂的控制流图是,使用区域计算公式V(G)=R,更为简单。
使用圈复杂度的好处
圈复杂度与软件程序的代码状况、可测性、维护成本的关系如下表。
1、使用圈复杂度作为提升代码质量的切入点。
通过圈复杂度,可以发现代码状况极复杂的模块或方法,这样的模块或方法也许可以进一步细化。
对于遗留代码的维护或重构,测量圈复杂度特别有价值。
2、限制程序逻辑过长。
McCabe&Associates 公司建议,尽可能使圈复杂度不超过10,即 V(G) <= 10。
NIST(国家标准技术研究所)认为在一些特定情形下,模块圈复杂度上限可放宽到 15 会比较合适。
因此圈复杂度 V(G)与代码质量的关系如下:
V(G) ∈ [ 0 , 10 ]:代码质量不错;
V(G) ∈ [ 11 , 15 ]:可能存在需要拆分的代码,应当尽可能想措施重构;
V(G) ∈ [ 16 , ∞ ):必须进行重构;
3、指导做测试计划和测试用例设计,确定测试重点。
许多研究指出,模块及方法的圈复杂度和其中的缺陷个数有正相关的关系,圈复杂度高的模块及方法,其中的缺陷个数也多,测试时需重点测试。
圈复杂度还为测试设计提供很好的参考。一个好的测试用例设计经验是:创建数量与被测代码圈复杂度值相等的测试用例,以此提升用例对代码的分支覆盖率。
4、TDD驱动编写简洁的代码
TDD(测试驱动的开发,test-driven development)和低圈复杂度值之间存在着紧密联系。在TDD模式下,编写代码时,开发人员会考虑代码的可测试性,倾向于编写简单的代码,因为复杂的代码难以通过测试。因此TDD的“测试用例、代码、测试、修改代码”循环将导致频繁重构,驱使非复杂代码的开发。
5、在持续集成(CI)中驱动代码重构
因为圈复杂度是如此好的一个代码复杂度指示器,在持续集成(CI)环境中,可以基于时间变化维度来评估模块或函数的复杂度和增长值。如果圈复杂度的值在不断增长,那么应该及时评估重构的必要性和具体方式,以降低出现代码维护问题的可能性。
圈复杂度的度量工具
圈复杂度的度量工具有很多,大致有三类,单语言的专用工具、多语言的通用工具和通用平台,如下表。
类型 |
名称 |
说明 |
专用工具(单语言) |
OCLint |
C语言相关 |
GMetrics |
Java |
|
PyMetrics |
python |
|
JSComplexity |
js |
|
通用工具(多语言) |
Lizard |
支持多种语言:C/C++ (works with C++14)、Java、C#、JavaScript、Objective C、Swift、Python、Ruby、PHP、Scala等。 |
SourceMonitor |
免费、Windows平台。支持语言包括C、C++、C#、Java、VB、Delphi和HTML。 |
|
通用平台 |
SonarQube |
一个用于代码质量管理的开源平台,支持20多种语言。通过插件机制可集成不同的测试工具,代码分析工具及持续集成工具 |
开发人员或测试人员可使用这些工具来度量和 告程序代码的圈复杂度。
McCabe复杂度
圈复杂度起源于McCabe复杂度。McCabe复杂度是成立于1976的McCabe & Associates公司开发出的软件复杂度评估技术。
McCabe复杂度是对软件结构进行严格的算术分析得来的,实质上是对程序拓扑结构复杂性的度量,明确指出了任务的复杂部分。
McCabe复杂度可以为软件开发过程中平衡成本、进度和性能提供指导,帮助工程师识别难以测试和维护的模块。McCabe复杂度已经成为评估软件质量的一个重要标准。
McCabe复杂度包括软件的七类复杂度。
(1)圈复杂度,Cyclomatic Complexity,v(G)
圈复杂度用来衡量一个模块的复杂程度。
(2)基本复杂度,Essential Complexity ,ev(G)
基本复杂度用来衡量程序非结构化程度。将模块控制流图中的结构化部分简化成节点,计算简化后控制流图的圈复杂度就是基本复杂度。
(3)模块设计复杂度,Module DesignComplexity ,iv(G)
模块设计复杂度用来衡量模块之间的调用关系,复杂度越高,模块之间耦合性越高,越难隔离,维护和复用。
从模块控制流图中移去那些不包含调用子模块的判定和循环结构后得到的圈复杂度。模块设计复杂度通常远小于圈复杂度。
(4)设计复杂度,Design Complexity ,S0
用来衡量程序模块之间的相互作用关系。设计复杂度等于程序中所有模块设计复杂度之和。
(5)集成复杂度,Integration Complexity,S1
集成测试的数量表示,也是程序中独立线性子树的数目。
计算方法:S1=S0-N+1, N是程序中模块的数目。
(6)行数,Number of Lines ,nl
模块中总的代码行数,包括注释。
(7)规范化复杂度,Normalized Complexity ,nv
规范化复杂度是圈复杂度和行数的比。nv=V(G)/nl
(8)全局数据复杂度
(9)局部数据复杂度
(10)病态数据复杂度
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!