寻求有关结构化代码以降低圈复杂度的说明

Posted

技术标签:

【中文标题】寻求有关结构化代码以降低圈复杂度的说明【英文标题】:Seeking clarifications about structuring code to reduce cyclomatic complexity 【发布时间】:2010-09-17 06:58:10 【问题描述】:

最近我们公司开始每周测量我们代码中函数的圈复杂度 (CC),并报告哪些函数得到了改进或恶化。所以我们开始更加关注函数的CC。

我读过 CC 可以非正式地计算为 1 + 函数中的决策点数(例如 if 语句、for 循环、选择等),或者也可以通过函数的路径数...

我知道减少CC最简单的方法是反复使用Extract Method重构...

有些事情我不确定,例如以下代码片段的CC是什么?

1)

for (int i = 0; i < 3; i++)
    Console.WriteLine("Hello");

Console.WriteLine("Hello");
Console.WriteLine("Hello");
Console.WriteLine("Hello");

他们都做同样的事情,但是第一个版本是否因为for语句而具有更高的CC?

2)

if (condition1)
    if (condition2)
        if (condition 3)
            Console.WriteLine("wibble");

if (condition1 && condition2 && condition3)
    Console.WriteLine("wibble");

假设语言做短路求值,比如C#,那么这两个代码片段的效果是一样的……但是第一个片段的CC是否更高,因为它有3个决策点/if语句?

3)

if (condition1)

    Console.WriteLine("one");

    if (condition2)
        Console.WriteLine("one and two");

if (condition3)
    Console.WriteLine("fizz");

if (condition4)
    Console.WriteLine("buzz");

这两个代码片段做了不同的事情,但是它们有相同的CC吗?还是第一个片段中的嵌套 if 语句有更高的 CC?即嵌套的 if 语句在心理上更难理解,但这是否反映在 CC 中?

【问题讨论】:

【参考方案1】:
    是的。您的第一个示例有一个决策点,而您的第二个没有,因此第一个示例的 CC 更高。 是的,也许您的第一个示例有多个决策点,因此 CC 更高。 (请参阅下面的说明。) 是的——也许吧。显然它们的决策点数量相同,但是计算 CC 的方法不同,这意味着......

...如果您的公司正在以特定方式衡量 CC,那么您需要熟悉该方法(希望他们正在使用工具来执行此操作)。对于不同的情况(case 语句、布尔运算符等)有不同的计算 CC 的方法,但是无论您使用什么约定,您都应该从度量中获得相同类型的信息。

更大的问题是其他人提到的,您的公司似乎更关注 CC 而不是其背后的代码。总的来说,当然,5以下很好,10以下很好,20以下还可以,21到50应该是一个警告信号,50以上应该是一个大警告信号,但这些是指导,不是绝对的规则。您可能应该检查 CC 高于 50 的过程中的代码,以确保它不仅仅是一大堆代码,但可能有特定原因导致该过程以这种方式编写,而且它不可行(对于任何原因)来重构它。

如果您使用工具重构代码以减少 CC,请确保您了解这些工具的作用,并且它们不是简单地将一个问题转移到另一个地方。最终,您希望您的代码几乎没有缺陷,能够正常工作,并且相对容易维护。如果该代码的 CC 也较低,那就太好了。如果您的代码符合这些标准并且 CC 高于 10,那么也许是时候坐下来尽您所能为您的代码辩护(也许让他们检查他们的政策)。

【讨论】:

【参考方案2】:

通过***条目和 Thomas J. McCabe 的 original paper 浏览后,您上面提到的项目似乎是该指标的已知问题。

但是,大多数指标确实有利有弊。我想在一个足够大的程序中,CC 值可能指向代码的可能很复杂部分。但更高的 CC 并不一定意味着复杂。

【讨论】:

您指的是谁的原始论文?能给个链接吗? 添加了链接和名称 (Thomas J McCabe) 谢谢,我浏览了这篇论文,虽然它采用精确的技术风格,但很难阅读。 ...我在 Wikipedia 或原始论文中找不到提到该指标已知问题的位置,请您指出正确的方向吗?【参考方案3】:

与所有软件指标一样,CC 并不完美。在足够大的代码库上使用,它可以让您了解可能在哪里是有问题的区域。

这里有两点要记住:

    足够大的代码库:在任何不平凡的项目中,您都会拥有具有非常高 CC 值的函数。如此之高,以至于在您的一个示例中,CC 是 2 还是 3 都没有关系。假设 CC 超过 300 的函数绝对值得分析。 CC 是 301 还是 302 无关紧要。 别忘了动动脑筋。有些方法需要很多决策点。通常可以以某种方式重构它们以减少它们,但有时它们不能。不要遵循“使用 CC > xy 重构所有方法”之类的规则。看看它们,然后用你的大脑来决定要做什么。

我喜欢每周分析的想法。在质量控制中,趋势分析是一种非常有效的工具,可以在在其产生过程中发现问题。这比必须等到它们变得如此之大以至于它们变得明显要好得多(有关详细信息,请参阅SPC)。

【讨论】:

是的,我们可以看到上周发生的变化,这非常好,因此您经常可以在列表中看到您处理过的函数,并记住您所做的更改并了解为什么CC增加了。不幸的是,我们的门槛很低,而且通常很难轻易降低 CC。 是的,这就是我在使用固定规则时遇到的问题。对于 80% 或 90% 的情况,它们可能没问题,但仍有 10% 的情况很糟糕。【参考方案4】:

CC 不是衡量质量的灵丹妙药。显然,重复语句并不比循环“更好”,即使循环具有更大的 CC。循环具有更大 CC 的原因是有时它可能会被执行,有时它可能不会,这导致两个不同的“案例”都应该被测试。在您的情况下,循环将始终执行三次,因为您使用了一个常量,但 CC 不够聪明,无法检测到这一点。

与示例 2 中的链式 ifs 相同 - 此结构允许您拥有一个语句,该语句仅在条件 1 和条件 2 为真时执行。这是一种特殊情况,在使用 && 的情况下是不可能的。因此,即使您没有在代码中使用它,if 链在特殊情况下也有更大的潜力。

【讨论】:

【参考方案5】:

这是盲目应用任何指标的危险。 CC 指标当然有很多优点,但与任何其他改进代码的技术一样,它不能脱离上下文进行评估。将您的管理层指向 Casper Jone 对代码行测量的讨论(希望我能为您找到一个链接)。他指出,如果代码行数可以很好地衡量生产力,那么汇编语言开发人员就是地球上生产力最高的开发人员。当然,他们并不比其他开发人员更有效率;只需要他们更多的代码来完成高级语言用更少的源代码所做的事情。正如我所说,我提到了这一点,这样您就可以向您的经理展示盲目地应用指标是多么愚蠢,而无需对指标告诉您的内容进行智能审查。

如果不是这样,我建议您的管理层明智地使用 CC 度量来发现代码中应进一步审查的潜在热点。盲目地追求降低 CC 的目标,而不参考代码可维护性或其他良好编码的措施是愚蠢的。

【讨论】:

“用代码行来衡量一个软件产品就像用它的重量来衡量一架飞机。” 比尔盖茨(我相信)
【参考方案6】:

圈复杂度类似于温度。它们都是度量,在大多数情况下,没有上下文就毫无意义。如果我说外面的温度是 72 度,那并没有多大意义;但如果我加上我在北极的事实,72 这个数字就变得很重要了。如果有人告诉我一个方法的圈复杂度为 10,那么如果没有它的上下文,我就无法确定它是好是坏。

当我对现有应用程序进行代码审查时,我发现圈复杂度是一个有用的“起点”指标。我首先检查的是 CC > 10 的方法。这些“>10”的方法不一定是坏的。它们只是为我提供了审查代码的起点。

考虑 CC 编号时的一般规则:

CC#和#of tests的关系,应该是CC# 仅在 CC# 增加时重构 可维护性 CC 10 以上往往表示一个或多个 Code Smells

【讨论】:

【参考方案7】:

[题外话] 如果您更喜欢指标的可读性而不是良好的分数(J.Spolsky 是否说过,“什么是衡量的,什么是完成的”? - 这意味着指标被滥用的频率比我想的要多),它是通常最好使用有名的布尔值来替换复杂的条件语句。

然后

if (condition1 && condition2 && condition3)
    Console.WriteLine("wibble");

成为

bool/boolean theWeatherIsFine =  condition1 && condition2 && condition3;

if (theWeatherIsFine)
    Console.WriteLine("wibble");

【讨论】:

是的,如果它提高了可读性,我通常会做这种事情。有趣的是,重构 - 改进现有代码的设计建议在条件而不是 Extract 变量上执行 Extract 方法...【参考方案8】:

我不是这方面的专家,但我想我会给我两分钱。也许这就是这一切的价值。

Cyclomatic Complexity 似乎只是一种特殊的自动快捷方式,可用于查找可能(但不是绝对)有问题的代码 sn-ps。但真正要解决的问题不就是测试吗?代码需要多少个测试用例?如果 CC 较高,但测试用例数量相同且代码更干净,则无需担心 CC。

1.) 那里没有决策点。那里只有一条通过程序的路径,两个版本中的任何一个都只有一个可能的结果。第一个更简洁更好,圈复杂度该死。

两个测试用例

2.) 在这两种情况下,你要么写“wibble”,要么不写。

两个测试用例

3.) 第一个可能没有结果,“一”或“一”和“一加二”。 3条路径。第二个可能不会导致任何结果,两者中的任何一个,或两者兼而有之。 4 条路径。

第一个测试用例 3 个 第二个用4个测试用例

【讨论】:

以上是关于寻求有关结构化代码以降低圈复杂度的说明的主要内容,如果未能解决你的问题,请参考以下文章

软件测试圈复杂度

降低圈复杂度

高圈复杂度会影响构建时间吗?

dotnet 代码优化 聊聊逻辑圈复杂度

.NET 代码优化 聊聊逻辑圈复杂度

OO游记之三月篇