了解环复杂性


11

我最近遇到了“ 圈复杂度”问题,我想尝试更好地理解它。

计算复杂度的不同因素有哪些实用的编码示例?具体来说,对于的Wikipedia等式M = E − N + 2P,我想更好地理解以下各个术语的含义:

  • E =图的边数
  • N =图的节点数
  • P =连接的组件数

我怀疑EN可能是代码块中决策点的数量(如果是,则是foreach等),但是我不确定是哪个代表另一个。我还猜测P是指函数调用和类实例化,但是鉴于我可以看到,没有一个明确的定义。如果某人可以通过一些清晰的代码示例来阐明更多信息,那将会有所帮助。

作为后续措施,环复杂性是否与100%路径覆盖所需的单元测试数量直接相关?例如,复杂度为4的方法是否表示需要4个单元测试才能覆盖该方法?

最后,正则表达式会影响环复杂度吗?


我发现您可以从Wikipedia上获得McCabe的原始论文,而Google Books将产生McCabe用于其原始论文的书。有趣的是,您会发现麦凯布(McCabe)错误地使用了原始定理(并且也引起了混乱的解释,因为他应该以无向图作为起点,并且不需要首先使其牢固地联系在一起),但是无论如何这些数字都是正确的(正确的公式应该是M = E + 1-N + P,但由于P始终为1,因此很适合...)发生这样的想法:现代的“异常处理”将扳手插入了该度量标准。
David Tonhofer,2014年

...以及递归调用(可能通过一系列函数)如何处理?有没有融合功能图?如何使布尔运算符“ &&”短路。受保护的运算符,例如“ ref?.x”,如果ref为null,则产生null?哦,这只是另一个指标。但是这里有一个大学项目的一些工作。
David Tonhofer 2014年

Answers:


8

关于公式:节点代表状态,边缘代表状态变化。在每个程序中,语句都会带来程序状态的变化。每个连续的语句都由一个边表示,执行该语句之后(或之前...)的程序状态为节点。

如果您有一个分支语句(if例如)-那么会有两个节点出现,因为状态可以以两种方式改变。

计算圈复杂度数(CCN)的另一种方法是计算执行图中有多少个“区域”(其中“独立区域”是一个不包含其他圆的圆)。在这种情况下,CCN将是独立区域的数量加1(这将与先前公式给出的数字完全相同)。

CCN用于分支覆盖或路径覆盖,这是相同的。CCN等于理论上在单线程应用程序中可能出现的不同分支路径的数量(该分支路径可能包括“ if x < 2 and x > 5 then”之类的分支,但应该由良好的编译器作为无法访问的代码捕获)。您必须至少具有不同数量的测试用例(可以多一些,因为某些测试用例可能会重复先前的用例所覆盖的路径,但是假设每个用例都覆盖一条路径就可以少得多)。如果您无法用任何可能的测试用例覆盖路径-您会发现无法访问的代码(尽管您需要向自己证明为什么它无法访问,可能是嵌套在x < 2 and x > 5某个地方)。

至于正则表达式-当然,它们会影响其他代码。但是,regex构造的CCN可能太高而无法在单个单元测试中涵盖,因此您可以假定regex引擎已经过测试,可以忽略满足测试需求的表达式分支潜力(除非您正在测试自己的正则表达式引擎)。


2
+1:实际上,您必须相信正则表达式引擎已经过测试。如果你不相信它,得到一个你做的信任。
S.Lott

“ CCN等于单线程应用程序中可能存在的不同执行路径的数量”,这是错误的,因为CCN仅基于代码的拓扑而不是其含义。这些路径中有很大一部分可能无法执行,因为它们要求无法设置的输入状态(例如,x为5 且还小于2)。坦率地说,我认为使用CCN来决定要运行的测试用例是错误的。CCN告诉开发人员“您可能在这里过分了,请考虑重构”。即便如此,也可能有较高的CCN。
David Tonhofer

1
@David添加了一个句子来解决这个问题。CCN是分支机构的覆盖范围,因此从来没有充分的理由在较低级别上获得较高的CCN(通常我建议根据每个功能强制执行)。
littleadv 2014年

分支覆盖范围和路径覆盖范围不同。分支覆盖旨在覆盖所有分支,而路径覆盖旨在覆盖分支的所有组合。
mouviciel

13

我懒散地写了一些关于此的评论...

具体来说,对于Wikipedia方程,M = E − N + 2P

这个等式是非常错误的

由于某种原因,McCabe确实在他的原始论文中使用了它(“复杂性测度”,IEEE Transactions on Software Engineering,Vo。SE-2,No.4,1976年12月),但没有给出正当理由,并且在引用了正确的文献后第一页上的公式

v(G)= e-v + p

(此处,公式元素已重新标记)

特别是,McCabe引用了《C.Berge,图和超图》一书(以下缩写为G&HG)。直接从那本书

定义(G&HG的第27页底部):

(无向)图G(可能具有多个断开的分量)的圈数v(G)定义为:

v(G)= e-v + p

其中e =边数,v =顶点数,p =连接的组件数

定理(《 G&HG》第29页顶部)(未被McCabe使用):

图G的圈数v(G)等于独立循环的最大数目

周期是开始,并在同一顶点结束,与序列中在图中彼此相邻的每两个连续的顶点的顶点序列。

直观地,如果没有一个循环可以通过叠加行走而与其他循环构成,则一组循环独立的。

定理(G&HG的第29页中间)(由McCabe使用):

在强连接图G中,圈数等于线性独立电路的最大数目。

电路是不允许有顶点和边的重复的循环。

如果每个顶点都可以通过沿其指定方向穿过边而从每个其他顶点到达,则称有向图是牢固连接的

请注意,这里我们从无向图传递到强连接图(有向图 ... Berge并未完全清楚这一点)

现在,McCabe应用上述定理得出一种简单的方法来计算“ McCabe环复杂度数”(CCN):

给定一个表示过程“跳转拓扑”的有向图(指令流图),其中指定的顶点表示唯一的入口点,指定的顶点表示唯一的出口点(可能需要“构造”出口点的顶点)通过在有多个返回的情况下将其相加),通过将从出口点顶点到入口点顶点的有向边添加有向边来创建强连接图,从而使入口点顶点可从任何其他顶点到达。

McCabe现在假定(我可能会说这有点令人困惑),修改后的指令流程图的圈数“符合我们对“最小路径数”的直观概念”,因此我们将使用该数作为复杂性度量。

很酷,所以:

可以通过对无向图中的“最小”电路进行计数来确定修改后的指令流程图的圈复杂度数。用人或机器并不是很难做到这一点,但是应用上述定理可以使我们更容易地确定它:

v(G)= e-v + p

如果忽略边缘的方向性。

在所有情况下,我们只考虑一个过程,因此整个图中只有一个连接的组件,因此:

v(G)= e-v + 1。

如果考虑原始图而没有添加“退出进入”边缘,则可以简单地获得:

ṽ(G)=ẽ-v + 2

如ẽ= e-1

让我们用他的论文中的McCabe的例子来说明:

麦凯比的例子

这里我们有:

  • e = 10
  • v = 6
  • p = 1(一个分量)
  • v(G)= 5(我们显然在计算5个周期)

圈数的公式表示:

v(G)= e-v + p

得出5 = 10-6 + 1,所以正确!

他的论文中给出的“ McCabe圈复杂度数”为

5 = 9-6 + 2(本文没有给出进一步的解释)

碰巧是正确的(它产生v(G)),但是由于错误的原因,即我们使用:

ṽ(G)=ẽ-v + 2

因此ṽ(G)= v(G)... phe!

但这有什么好处吗?

用两个词来说:不是很

  • 尚不清楚如何建立程序的“指令流程图”,特别是在异常处理和递归进入画面的情况下。请注意,McCabe将他的想法应用到了用FORTRAN 66编写的代码中,该语言没有递归,没有异常且具有简单的执行结构。
  • 具有决策的过程和具有循环的过程产生相同的CCN的事实并不是一个好兆头。

在此处输入图片说明


1
@JayElston好收获。确实,我愿意。固定!
David Tonhofer

1
大+1,用于链接到原始论文。大约那时写的许多论文对于任何中级程序员都是可读的,应该阅读。
Daniel T.

1

作为后续措施,环复杂性是否与100%路径覆盖所需的单元测试次数直接相关?

是的,基本上。利用圈复杂度作为何时进行重构的指标也是一个好主意。以我的经验,较低的CC会大大提高可测试性和可重用性(尽管您应该很实际-请勿过度重构,并且某些方法由于其性质而具有较高的CC-尝试强制实施这种方法并非总是有意义的降低)。

最后,正则表达式会影响环复杂度吗?

是的,如果您想精确一点,尽管大多数代码分析工具似乎都没有以这种方式考虑它们。正则表达式只是有限状态机,因此我猜想它们的CC可以从FSM图中计算出来,但是它的数量很多。


+1-我猜想为RegExes计算CC并不是一件有趣的事情。
VirtuosiMedia
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.