【代码质量】认知复杂度(COGNITIVE COMPLEXITY)一种衡量可理解性的新方法

news2025/1/19 17:20:45

白皮书地址

摘要:圈复杂度最初是作为“可测试性和模块控制流的“可维护性”。虽然它擅长于衡量前者,但它的数学模型不能产生一个令人满意的值来衡量后者。本文描述一种打破数学度量模型的新度量模型来评估代码,以弥补圈复杂度的缺点,更准确地反映理解难度的测量方法,对于方法、类和应用程序都是有效的。

1、介绍

Thomas J. McCabe的圈复杂度长期以来,作为测量方法控制流的复杂性的标准。它最初的目的是“识别”软件模块将难以测试或维护”,同时它可以精确计算完全覆盖一个方法所需的最小测试用例数量,但是,它对可理解性的度量满足不了人们需要。这是因为具有相同圈复杂度的方法,不一定会给维护者带来同样的困难,导致一种感觉通过高估某些结构,而低估其他结构。同时,圈复杂度不再是全面的。用一种在1976年的Fortran环境中,它不包括现代语言结构,比如try/catch和λ表达式。最后,因为每种方法的最小圈复杂度得分为1,所以它是不可能知道一个具有高的聚合圈复杂度的类到底是一个易于维护的大类,还是一个具有复杂控制流的小类。除了在类水平上,人们普遍认为应用程序的圈复杂度得分,是与其代码总数相关联。换句话说,圈复杂度是在方法级别以上很少使用。作为这些问题的补救措施,认知复杂性已经制定了解决现代语言结构的方案,并在类和应用程序水平上产生有意义的价值。更重要的是,它偏离了基于评估代码的数学模型,使它可以产生评估的控制流与程序员对理解这些代码所需要的心理或认知努力的认知流对应起来。

1.1 问题讨论

用一个例子来开始引出复杂性的讨论,以下两种方法具有相同的圈复杂度,但是在可理解性方面有显著不同。

int sumOfPrimes(int max) { // +1
int total = 0;
OUT: for (int i = 1; i <= max; ++i) { // +1
for (int j = 2; j < i; ++j) { // +1
if (i % j == 0) { // +1
continue OUT;
}
}
total += i;
}
return total;
} // 圈复杂度 4
String getWords(int number) { // +1
switch (number) {
case 1: // +1
return "one";
case 2: // +1
return "a couple";
case 3: // +1
return “a few”;
default:
return "lots";
}
} //  圈复杂度 4

基于数学模型的圈复杂度,这两个方法的圈复杂度是相等的。然而,直观上很明显,sumofprime的控制流程比getWords更难理解。这就是为什么认知复杂度(Cognitive Complexity)放弃这种基于评估控制流的数学模型,而去使用一套简单、面向程序员直觉的规则。

1.2 基本准则及方法

认知复杂性评分是根据以下三个基本规则:

  • 忽略简写:把多句代码缩写为一句可读的代码,不改变理解难度;
    Ignore structures that allow multiple statements to be readably shorthanded into one

  • 对线性的代码逻辑中,出现一个打断逻辑的东西,难度+1;
    Increment (add one) for each break in the linear flow of the code

  • 当打断逻辑的是一个嵌套时,难度+1;
    Increment when flow-breaking structures are nested

以下四种不同类型,均会使认知复杂度得分加一:

  • Nesting:把一段代码逻辑嵌套在另一段逻辑中;
  • Structural:被嵌套的控制流结构;
  • Fundamental:不受嵌套影响的语句;
  • Hybrid:一些控制流结构,但不包含在嵌套中;

然而不同类型在数学上没有区别,都只是对复杂度加一。在要计算的不同类别之间进行区分,可以更轻松地了解某一处是否适用嵌套的逻辑。以下各节将进一步详细说明这些规则及其背后的原理。

1.2.1 忽略简写

认知复杂性的一个指导原则是,鼓励良好的编码实践。也就是说,需要无视或低估让代码更可读的feature(不进行复杂度计算)。“方法”本身就是一个朴素的例子,把一段代码拆的把几句抽离成一个方法,用一句方法调用代替掉,“简写”它,认知复杂度不会因为这这一次方法调用增加。同样的,认知复杂度也会忽略掉null-coalescing操作符,x?.myObject这样的操作符不增加复杂度,因为这些操作同样是把多段代码缩写为一项了。例如下面的两段代码:

MyObj myObj = null;
if (a != null) {
myObj = a.myObj;
}
MyObj myObj = a?.myObj

第一个的版本的意思需要一些时间来处理,而下边的版本一旦理解了空合并语法,下边就一目了然了。因此,认知复杂性忽略了null-coalescing操作符。

1.2.2(Increment for breaks in the linear flow) 打断线性代码流程导致的复杂

在认知复杂度的制定想法中,另一项指导原则是:结构控制会打断一条线性的流从头到尾走完,使代码的维护者需要花更大功夫来理解代码。在认定了这会导致额外负担的前提下,认知复杂度评估了以下几种会增加Structural类复杂度:

  • 循环: for, while, do while, ...
  • 条件: 三元运算符, if, #if, #ifdef...
    另外,以下这种会计处Hybrid类复杂度: else if, elif, else, ...

但不计入Nesting类复杂度,因为这个量在计算之前的if语句时已经计过了。
这些增加复杂度,其实和圈复杂度的计算方式非常像,但是额外的,认知复杂度还会计算:

1.2.2.1 Catches

一个catch表达了控制流的一个分支,就像if一样。因此每个catch语句都会增加Structural类的认知复杂度,仅加1分,无论它catch住多少种异常。(在我们的计算中try\finally被直接忽略掉)

1.2.2.2 Switches

一个switch语句,和它附带的全部case绑在一起记为一个Structural类,来增加复杂度。
在圈复杂度下,一个switch语句被视为一系列的if-else链,因此每个case都会增加复杂度,因为会使得控制流分支增多。
但以代码维护的视角来看,一个switch:将单个变量与一组显式的值比较,要比if-else链易于理解,因为if-else链可以用任意条件做比较。就是说if-else if链必须仔细的逐个阅读条件,而switch通常是可以一目了然的。

1.2.2.3 Sequences of logical operators (一系列的逻辑操作)

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

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

理解后一行的操作,不会比理解前一行的操作更难。但是对于下面两行,理解难度有质的区别:

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

因为boolean操作表达式混合使用时会更难理解,因此对这类操作每出现一个,认知复杂度都会不断递增。例如:

if (a            // +1 for `if`
&& b && c        // +1
|| d || e        // +1
&& f)            // +1
if (a            // +1 for `if`
&&               // +1
!(b && c))       // +1

尽管认知复杂度相对于循环复杂度,为类似的运算符提供了“折扣”,但它可以为所有的布尔运算符都有所增加。(例如那些变量赋值,方法调用和返回语句)

1.2.2.4 Recursion(递归)

与圈复杂度不同,认知复杂度对每一个递归调用,都增加一点Fundamental类复杂计分,不论是直接还是间接的。有两个这样做的动机:
1、递归表达了一种“元循环”,并且循环会增加认知复杂度;
2、认知复杂度希望能用于估计一个方法,其控制流难以理解的程度,而即使是一些有经验的程序员,都觉得递归难以理解;

1.2.2.5 Jumps to labels

goto, break与continue到某处label,会增加Fundamental类复杂程度。但是在代码过程中提前return,可以使代码更清晰,所以其它类型的continue\break\return都不会导致复杂程度增加。

1.2.3 Increment for nested flow-break structures(嵌套打断思路造成的复杂)

直觉看起来很明显,由连续嵌套的五个结构比,线性连续的五个if\for结构要好理解得多(不考虑执行路径上的第一个部分有几句代码)。因为这样的嵌套会增加理解代码的成本,所以认知复杂度在计算时会将其视为一个Nesting类复杂度增加。特别地,每一次有一个导致了Structural类或Hybrid类复杂的结构体,嵌套了另一个结构时,每一层嵌套都要再加一次Nesting类复杂度。例如下面的例子,这个方法本身和try这两项就不会计入Nesting类的复杂,因为它们即不是Structure类也不是Hybrid类的复杂结构:

void myMethod () {
try {
if (condition1) { // +1
for (int i = 0; i < 10; i++) { // +2 (nesting=1)
while (condition2) {} // +3 (nesting=2)
}
}
} catch (ExcepType1 | ExcepType2 e) { // +1
if (condition2) {} // +2 (nesting=1)
}
} // Cognitive Complexity 9

然而,对于if\for\while\catch这些结构,全部被视为Structural类和Nesting类的复杂。此外,虽然最外层的方法被忽略了,并且lambda、#ifdef等类似功能也都不会视为Structral类的增量,但是它们会计入嵌套的层级数:

void myMethod2 () {
Runnable r = () -> { // +0 (but nesting level is now 1)
if (condition1) {} // +2 (nesting=1)
};
} // Cognitive Complexity 2
#if DEBUG // +1 for if
void myMethod2 () { // +0 (nesting level is still 0)
Runnable r = () -> { // +0 (but nesting level is now 1)
if (condition1) {} // +3 (nesting=2)
};
} // Cognitive Complexity 4

1.2.4 The implications 含义

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

1.2.4.1 Intuitively ‘right’ complexity scores( 直觉上对的复杂分)

在本篇开头的时候讨论了两个圈复杂度相同的方法(但它们有着完全不同的理解难度),现在回过头来检查一下这两个方法的认知复杂度:
在这里插入图片描述
认知复杂度算法,给这两个方法完全不同的得分,这个得分结果更接近它们的相对理解成本。

1.2.4.2 Metrics that are valuable above the method level (方法级别之上也有用的指标)

更进一步的,因为认知复杂度不会因为方法这个结构增加,复杂度的总和开始有用了起来。现在你可以看出两个类:一个有大量的getter()\setter()方法,另一个类仅有一个极其复杂的控制流,可以简单的通过比较二者的认知复杂度就行了。认知复杂度可以成为衡量一个类或应用的相对复杂度的工具。

2、Conclusion (结论)

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/884296.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【科研论文配图绘制】task1 掌握科研绘图的基本知识

【科研论文配图绘制】task1 掌握科研绘图的基本知识 写在最前 8月份Datawhale组队学习&#xff0c;写下该博客记录学习内容 1.科研论文配图的分类与构成 2.科研论文配图的格式和尺寸 3.科研论文配图中的字体和字号设置 4.科研论文配图的版式设计、结构布局和颜色搭配 占个…

【校招VIP】CSS校招考点之选择器优先级

考点介绍&#xff1a; 选择器是CSS的基础&#xff0c;也是校招中的高频考点&#xff0c;特别是复合选择器的执行优先级&#xff0c;同时也是实战中样式不生效的跟踪依据。 因为选择器的种类较多&#xff0c;很难直接记忆&#xff0c;可以考虑选择一个相对值&#xff0c;比如id类…

day4 IO模型

IO多路复用 1.select函数 服务器&#xff1a; 客户端 poll函数 客户端&#xff1a;

《Java-SE-第三十八章》之注解

前言 在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!” 博客主页&#xff1a;KC老衲爱尼姑的博客主页 博主的github&#xff0c;平常所写代码皆在于此 共勉&#xff1a;talk is cheap, show me the code 作者是爪哇岛的新手&#xff0c;水平很有限&…

每日记--前端解决方案--el-select下拉样式-el-option内容过长-鼠标悬停到文字不修改光标样式-设置透明

文章目录 el-select下拉样式el-select中el-option内容过长解决办法鼠标悬停到文字不修改光标样式设置透明 el-select下拉样式 element-ui自带样式设置popper-class el-select中el-option内容过长解决办法 问题&#xff1a;像这样选项太长了&#xff0c;不好看 解决&#xf…

关于Linux文件系统只读问题的修改笔记

1.问题 2. 原因 系统异常关机或者代码修改错误导致硬盘挂载出现问题开启只读模式&#xff0c;但是重启有时候可以解决。 3.解决方法 1. mount查看那个挂载的硬盘出现问题(ro标识只读) mount | grep ro2.找到硬盘&#xff0c;重新挂载即可 sudo mount -o remount,rw /sys/f…

半导体市场震荡,硅晶圆价格下修成焦点 | 百能云芯

半导体市场状况不容乐观&#xff0c;原本被半导体晶圆制造厂视为稳定业绩的长期合同开始面临松动。行业内传出&#xff0c;国内重要的晶圆代工大厂已向日本硅晶圆供应商提出要求降低明年合同价格的请求&#xff0c;以共同应对困境&#xff0c;双方目前正处于激烈的博弈中。鉴于…

测试相关Liunx基础知识

Linux的历史和安装 基本常识 Liunx目录结果 常见

1€滤波器(1 Euro Filter)使用介绍

怎么调整欧拉角x、y、z的抖动问题&#xff1f;

python+django+mysql项目实践四(信息修改+用户登陆)

python项目实践 环境说明: Pycharm 开发环境 Django 前端 MySQL 数据库 Navicat 数据库管理 用户信息修改 修改用户信息需要显示原内容,进行修改 通过url传递编号 urls views 修改内容需要用数据库的更新,用update进行更新,用filter进行选择 输入参数多nid,传递要修…

数据结构--有向⽆环图 描述表达式

数据结构–有向⽆环图 描述表达式 有向⽆环图 \color{red}有向⽆环图 有向⽆环图&#xff1a;若⼀个有向图中 不存在环 \color{red}不存在环 不存在环&#xff0c;则称为有向⽆环图&#xff0c;简称 D A G 图 \color{red}DAG图 DAG图&#xff08;Directed Acyclic Graph&#x…

2021年09月 C/C++(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

第1题&#xff1a;字符统计 给定一个由a-z这26个字符组成的字符串&#xff0c;统计其中哪个字符出现的次数最多。 输入 输入包含一行&#xff0c;一个字符串&#xff0c;长度不超过1000。 输出 输出一行&#xff0c;包括出现次数最多的字符和该字符出现的次数&#xff0c;中间以…

autodock后的pdbqt文件怎么通过网站分析?

首先需要在pymol中打开这个docking后的分子 然后再打开受体&#xff0c; 注意&#xff1a;顺序不要反&#xff0c;顺序反了会导致网址分析错误 最后导出为pdb就可以了放在网站上用了 网址&#xff1a;https://plip-tool.biotec.tu-dresden.de/plip-web/plip/index

RK3568KK操作手册

一&#xff0e;烧录MCU 板子不用上电&#xff0c;接上烧录器 打开 HOPE3000 For e-Link 烧录软件。选择文件&#xff0c;选择要烧录的固件&#xff1a;HT66F2030.MTP 选择3V 点击下载&#xff0c; 点击所有 烧录成功如图所示&#xff1a; 二&#xff0e;接上电源&am…

什么是自动化测试?如何做自动化测试?

前面介绍了功能测试和接口测试&#xff0c;在介绍接口测试时提到了实现API自动化。那具体什么是自动化&#xff0c;为什么要做自动化&#xff0c;这里我们集中总结。 一. 什么是自动化&#xff1f; 顾名思义&#xff0c;自动化测试是相对人工测试而言的&#xff0c;它是指把人…

易云维®医院后勤一站式服务平台实现对医院人、物、设备进行信息化管理

传统后勤移动系统的缺陷 使用的门槛和成本高。在国内只有一些大医院开展及应用&#xff0c;由于传统移动运维系统需要定制软件、结合专用平板使用&#xff0c;导致整体项目价格昂贵&#xff0c;故一般采购医院配置的平板少&#xff0c;从而影响记录实时互动追踪的效果&#xf…

如何克服预测性维护中IT和OT的融合挑战?

预测性维护&#xff08;Predictive Maintenance&#xff0c;简称PdM&#xff09;在现代制造业中扮演着关键角色&#xff0c;通过实时数据分析和资产监控&#xff0c;帮助企业预测设备故障&#xff0c;优化维护计划&#xff0c;并提高生产效率。然而&#xff0c;PdM的成功实施面…

无法解析的外部符号cusolverDnCreate

问题&#xff1a; 无法解析的外部符号cusolverDnCreate 解决方案 那么就在启动项目-》属性-》连接器-》输入-》附加依赖项&#xff1a;加&#xff1a; cublas.lib cublas_device.lib cuda.lib cudadevrt.lib cudart.lib cudart_static.lib cufft.lib cufftw.lib curand.lib …

Flink 流式读写文件、文件夹

文章目录 一、flink 流式读取文件夹、文件二、flink 写入文件系统——StreamFileSink三、查看完整代码 一、flink 流式读取文件夹、文件 Apache Flink针对文件系统实现了一个可重置的source连接器&#xff0c;将文件看作流来读取数据。如下面的例子所示&#xff1a; StreamExe…

编写一个通用函数,从键盘输入n,显示正n边形。通过调用函数,在屏幕上同时显示下面的四个图形

题目&#xff1a;编写一个通用函数&#xff0c;从键盘输入n&#xff0c;显示正n边形。通过调用函数&#xff0c;在屏幕上同时显示下面的四个图形。 结果&#xff1a; 调用举例&#xff1a; drawShape(3)drawShape(4)drawShape(5)drawShape(6) 代码&#xff1a; import turt…