认知复杂度——估算项目代码的理解成本

目录

    • 1. 摘要
    • 2. 历史背景
      • 2.1 工程复杂度背景
      • 2.2 有哪些问题待解决
      • 2.3 一个例子
    • 3. 认知复杂度解释
      • 3.1 基本原则
      • 3.2 忽略语法糖
      • 3.3 各种类型的认知复杂
        • 3.3.1 `Structural类复杂度`:
        • 3.3.2 Hybrid类复杂度
        • 3.3.3 一连串的逻辑操作
        • 3.3.4 Fundamental类复杂度
        • 3.3.5 Nesting复杂度
      • 3.4 认知复杂度的目标
        • 3.5.1 认知复杂度的示例
        • 3.5.2 方法之上的级别
      • 3.6 Conclusion 结论
    • 4. 其它
      • 4.1 计算认知复杂度的示例
      • 4.2 引用

1. 摘要

Cognitive Complexity:认知复杂度,是由sonarQube设计的一个算法,算法将一段程序代码被理解的复杂程度,估算成一个整数——可以等同于代码的理解成本。
IntellliJ IDEA支持sonar插件,可以实时计算函数的认知复杂度,当复杂度过高时将提醒重构。

认知复杂度的计算基于以下三规则:语法糖不增加理解难度、打断线性代码执行增加理解难度、多层嵌套增加理解难度。

2. 历史背景

(此部分可略过)

2.1 工程复杂度背景

最初工业界普遍使用圈复杂度(Cyclomatic Complexity)来描述一段代码逻辑的“可测性与可维护性”,尽管用它来描述“可测性”很好(可测性的意思是:需要构建出完美的单元测试需要多少代价),但这种模型不能很好的描述代码的“可维护性”。
认知复杂度很好的弥补了“圈复杂度”的这个缺点,用一种全新的、简单的度量方法,准确地反映代码的理解成本,以及维护的难度。
“认知复杂度”与编程语言无关,它可以用来测量文件、类、过程与函数等等概念下的复杂度。

2.2 有哪些问题待解决

  • Thomas J. McCabe设计的圈复杂度是一个业界标准,最初目的是用来识别“难以测试和维护的软件模块”——但是用这种算法只能算出一个模块最少的全覆盖测试用例数量,而不能算出一个精确的模块“理解难度”。因此圈复杂度一样的两段代码,维护难度却有可能天差地别,用圈复杂度来理解维护难度,导致我们对模块代码有错误估计。
  • 圈复杂度的理论是在1976年于Fortran语言环境下设计的,如今使用它来衡量新语言不再是那么全面了。一些现代的语言结构,如try-catch与lambda没有被考虑在内。
  • 每个方法都的最小圈复杂度都是1,导致了“圈复杂度”与其方法数量是相关。一个很好理解的Class(类),也可能因为包含多个简单方法,总复杂度被抬得很高。
    为了解决这些问题,SonarQube制定了认知复杂度(Cognitive Complexity),一方面解决了圈复杂度在现代语言结构的不足,一方面使复杂度在方法、类、应用程序级别都有实际意义。 更重要的是,这个复杂度值与程序员理解这些代码片段所需的直觉(理解难度)相对应。

2.3 一个例子

这里给出一个很有用的例子,可以指出圈复杂度的问题。以下两段方法有着相同的圈复杂度,但是在理解难度上差非常多:
认知复杂度——估算项目代码的理解成本_第1张图片
圈复杂度理论,对上图的两个方法给出等同的复杂度,然而从直觉上显然左边的sumOfPrimes要更难以理解一些。这也是为什么认知复杂度舍弃了使用数学模型来评估一段逻辑,改用一组简单的规则,把代码的直觉理解程度转为一个数字表达。

3. 认知复杂度解释

3.1 基本原则

(3.1小节仅作为规则提炼,细节解释见3.2-3.6)

认知复杂度的评估分数,是基下面三条基本规则:

  1. 忽略简写:把多句代码缩写为一句可读的代码(语法糖),不改变理解难度;
  2. 打断线性的代码逻辑:出现一个打断逻辑线性执行的语句,难度+1;
  3. 当打断逻辑的是一个嵌套时,难度+1;

以下四种不同类型,均会使认知复杂度得分加一:
A. Nesting:把一段代码逻辑嵌套在另一段逻辑中;
B. Structural:被嵌套的控制流结构;
C. Fundamental:不受嵌套影响的语句;
D. Hybrid:一些控制流结构,但不包含在嵌套中;

3.2 忽略语法糖

认知复杂度的指导性的原则是:鼓励使用者写出好的编码规范。因此让代码更可读的feature,是不需要计入复杂度的。比如一次简单的方法调用,又或者使用语法特性完成的一个可理解动作。
比如null-coalescing操作符x?.myObject,如下图代码),就是属于语法糖,是不会增加认知复杂度的:
认知复杂度——估算项目代码的理解成本_第2张图片
左侧的代码会因为if语句打断了逻辑,使得理解难度上升了,因此认知复杂度不为0;
右侧的代码逻辑是可以直接理解的语法糖,因此复杂度为0。

3.3 各种类型的认知复杂

认知复杂度的另一项指导原则:控制流会打断一条线性的执行流——程序不再是一行行往下走了,因此代码的维护者需要花更大功夫来理解它。

3.3.1 Structural类复杂度

一、循环: for, while, do while, …
二、条件: 三元运算符, if, #if, #ifdef…
三、Catch:
一个catch表达了控制流的一个分支,就像if一样。因此每个catch语句都会增加Structural类的认知复杂度,仅加1分,无论它catch住多少种异常。(在我们的计算中try\finally被直接忽略掉)
四、Switch:
一个switch语句,和它附带的全部case绑在一起记为一个Structural类,复杂度仅增加1。(不像圈复杂度,每个case都会使得控制流分支增加,进而增加圈复杂度)
Switch可以视为用单个变量与一组值作匹配,是一目了然的,因此要比if-else链更容易理解,因此认知复杂度更低。

3.3.2 Hybrid类复杂度

  • else if, elif
  • else …

计算为Hybrid类复杂度以后,不会再计入Nesting类复杂度,因为嵌套的复杂程度在if语句时候已经计入了。(见3.3.5)

3.3.3 一连串的逻辑操作

认知复杂度不对每一个逻辑运算符计分,而是考虑对连续的一组逻辑操作加分。例如下面几个操作:

a && b
a && b && c && d
a || b
a || b || c || d

理解后一行的操作,不会比理解前一行的操作更难,上面的四种认知复杂度都是1。但是对于下面两行,理解难度有质的区别:

a && b && c && d
a || b && c || d

这是因为boolean操作表达式混合使用时,难度会显著上升,因此认知复杂度的值会不断递增。具体计算方法如下:
认知复杂度——估算项目代码的理解成本_第3张图片
尽管认知复杂度相对于循环复杂度,为类似的运算符提供了“折扣”,但它可以为所有的布尔运算符都有所增加。(例如那些变量赋值,方法调用和返回语句)

3.3.4 Fundamental类复杂度

以下几种均为Fundamental类,会将认知复杂度+1
一、递归
与普通的调用函数不同,每一个递归调用,都增加一点Fundamental类复杂度计分,不论是直接还是间接的。有两个这样做的动机:

  • 递归表达了一种“元循环”,并且循环会增加认知复杂度;
  • 认知复杂度希望能用于估计一个方法,其控制流难以理解的程度,而即使是一些有经验的程序员,都觉得递归难以理解;
    二、Jump类语句
    goto, break与continue到某处,都会增加Fundamental类复杂程度。但是在代码过程中提前return,可以使代码更清晰,所以其它类型的continue\break\return都不会导致复杂程度增加。

3.3.5 Nesting复杂度

直觉上连续嵌套的代码,要比线性代码难理解很多。这样的嵌套会增加理解代码的成本,所以认知复杂度在计算时会将其单独归类,视为一个Nesting类的复杂度增加。
当一个Structural类或Hybrid类的结构体,嵌套其它逻辑时,每一层嵌套都要额外计入一次Nesting类复杂度。这样说有点抽象,可以结合下面的例子来理解:
认知复杂度——估算项目代码的理解成本_第4张图片
这个方法(myMethod方法)与try这两项,是不会计入Nesting类的复杂的,因为它们即不是Structure类也不是Hybrid类的复杂结构:
然而,对于if\for\while\catch这些结构是Structural类,再与Nesting类结构结合,就会按递增的增加复杂度(+1 +2… +n)。
此外,lambda、#ifdef等类似代码虽然不是Structural类型,但是它们会增加嵌套的层数,如下例所示:
认知复杂度——估算项目代码的理解成本_第5张图片

3.4 认知复杂度的目标

认知复杂度制定的主要目标,是为方法计算出一个得分,准确地反应出此方法的相对理解难度。它的次要目标,是解决现代语言结构的问题,并产生在方法级别以上也有价值的指标。 可以证明,解决现代语言结构的目标已经实现。 其他两个目标在下面进行了检查。

3.5.1 认知复杂度的示例

在本篇开头的时候讨论了两个圈复杂度相同的方法,它们可能有着完全不同的理解难度,那么认知复杂度的表现如何呢?
认知复杂度——估算项目代码的理解成本_第6张图片
上面两个有着相同圈复杂度的代码,却有着完全不同的认知复杂度。左侧得分为7,右侧则为1,这个结果比较接近它们的真实理解成本。

3.5.2 方法之上的级别

因为认知复杂度不会因为方法这个结构增加,复杂度总和的指标变得更有意义。一个有大量的getter()\setter()方法组合的类,拥有较低的认知复杂度,另一个类仅一个极其复杂的控制流的方法,会拥有着较大的认知复杂度。
我们可以简单的计算一个类、一个模块、一个应用的认知复杂度总和,用这个总和就可以估计出总体理解成本。

3.6 Conclusion 结论

编写和维护代码是一个人为过程,它们的输出必须遵守数学模型,但它们本身不适合数学模型。 这就是为什么数学模型不足以评估其所需的工作量的原因。
认知复杂性不同于使用数学模型评估软件可维护性的实践。 它从圈复杂度设定的先例开始,但是使用人工判断来评估应如何对结构进行计数,并决定应向模型整体添加哪些内容。 结果,它得出的方法复杂性得分比以前的模型更能吸引程序员,因为它们是对可理解性的更公平的相对评估。 此外,由于认知复杂性不收取任何方法的“入门成本”,因此它不仅在方法级别,而且在类和服务级别,都产生了更加准确的评估结果。

4. 其它

4.1 计算认知复杂度的示例

认知复杂度——估算项目代码的理解成本_第7张图片

4.2 引用

sonar官网 白皮书
code climate官网 条目解释

你可能感兴趣的:(工程质量)