Thomas J. McCabe 在 1976 年提出了圆环复杂度(Cyclomatic Complexity),作为指导程序员编写“既可测试又可维护”的方法。在SonarSource,我们认为圆环复杂度在测量可测性方面效果很好,但不适合测试可维护性。 这就是我们提出认知复杂(Cognitive Complexity)的原因,您将在即将到来的新版语言分析器中看到这一点。 我们用认知复杂度来更好地衡量代码的难以理解的程度。
圆环复杂度 不能衡量可维护性
这两种方法具有相同的圆环复杂度,但显然不具有相同的可维护性。 当然,这种比较可能不完全公平; 甚至 McCabe 在他的原始论文中也承认,在 switch 语句中 case 标签的复杂度计算似乎并不正确。
另一方面,这正是圆环复杂度的问题。 这些分数肯定会告诉你需要多少测试用例才能覆盖给定的方法,但从可维护性的角度来看,它们并不总是正确的。 此外,即使最简单的方法得到的圆环复杂度也为1,一个大型的类可能与充满强烈逻辑的小类具有相同的圆环复杂度。 研究表明,在应用层面圆环复杂度与代码行相关,所以它实际上并没有告诉你任何新东西。
用认知复杂度解决问题吧
这就是为什么我们制定了认知复杂度,它试图将一个方法的控制流程有多难以理解进行衡量并降低此复杂性。
我会在一分钟内详细介绍一些细节,但首先我想谈一下我们为什么要制定认知复杂度。显然,我们的主要目标是将计算的可维护性进行直观“公平”地表示。 但是,我们非常清楚的知道,当我们衡量了之后,我们就会尝试去改进它。正因为如此,我们希望认知复杂度能够通过改善代码结构来获得良好的干净的编码实践,这些代码结构需要额外的努力去理解,我们应该忽略那些使代码更易于阅读的结构。
我们把这个指导思想归结为三条简单的规则:
- 当线性的(从上到下,从左到右)代码结构中存在中断时将认知复杂度加1
- 当内嵌了打断工作流的代码结构时将认知复杂度加1
- 忽略那些将多行代码压缩为一行代码的结构
示例:
将以上三个规则记在心里,让我们一起分析下面两个方法。
所提到的,用圆环复杂度衡量代码可读性的其中一个最不合理地方在于它对 switch 语句的处理。而在认知复杂度中,对于一整个 switch 结构(包括了所有的 case 语句),其度量值只增加一次。为什么只增加一次呢?简短来说,因为认知复杂度是用来衡量代码难以理解的程度,而 switch 语句很容易被理解。另一方面,对于其他的流程控制语句,认知复杂度规则以类似的方式对待,如循环语句 for、while、do while;三元操作符 if/#if/#ifdef…else,if/elsif/elif/…,还有 catch 语句。另外当遇到 goto、break、continue 标签,内嵌的控制流时复杂度也会增加。
正如你所看到的,认知复杂度考虑了使方法更难理解的东西 —— 嵌套语句和 continue 语句。因此,尽管以上示例中的两种方法具有相同的环复杂度分数,但他们的认知复杂度分数清楚地反映了他们在可理解性方面的巨大差异。
在查看这些示例时,您可能已经注意到,认知复杂度不会因为方法本身增加。这意味着简单的类的认知复杂度为零:
所以现在的类级别的复杂度指标变得有意义。您可以查看类列表和他们的认知复杂度分数,并且知道当您看到一个很高的数字时,这意味着 class 中有很多逻辑,而不仅仅是有很多方法。
开始使用 Cognitive 复杂度吧
此时,你已经对认知复杂度有一定高度了解。布尔运算符的认知复杂度计算方式存在一些差异,但是你可以阅读白皮书来获得这些细节。希望你渴望开始使用认知复杂度,并想知道何时可以使用能够测量认知复杂度的工具。
我们将开始在各种语言中添加方法级别的认知复杂度规则,类似于现有的圆环复杂度规则。您将在主要的几种语言中看到:Java,JavaScript,C#和C / C ++ / Objective-C。同时,我们将纠正现有的方法级别的“圆环复杂性”规则的实现,以真正衡量圆形复杂度(现在,它们是圆形和基本复杂度的组合)。
最终,我们可能会添加类/文件级别的认知复杂度规则和指标。但我们先从添加方法级别的复杂度规则开始吧!
最后
推荐一个 vscode 插件用于计算 js 代码复杂性,提升重构乐趣
CodeMetrics
参考: