常见代码复杂度解析

简介: 代码质量评价维度,很多都是些主观性的评价维度,需要有专门的人员去查看评判代码,对于审核的人员代码能力要求比较高,而且有时候往往不同的人审核会得出不同的结论,会有争议。然而也有些对代码客观的分析方式可以帮助我们识别代码质量,节省大量人力去分析代码。比如代码复杂度的分析。

代码质量评价维度,很多都是些主观性的评价维度,需要有专门的人员去查看评判代码,对于审核的人员代码能力要求比较高,而且有时候往往不同的人审核会得出不同的结论,会有争议。然而也有些对代码客观的分析方式可以帮助我们识别代码质量,节省大量人力去分析代码。比如代码复杂度的分析。

判断代码复杂度有多种不同的方式,下面介绍一些其中比较重要的判断代码复杂度的方式:

一 圈复杂度(Cyclomatic complexity,V(G))

1.1 圈复杂度介绍

圈复杂度(Cyclomatic complexity)也称为条件复杂度或循环复杂度,是一种软件度量,是由Thomas J. McCabe在1976年提出,用来表示程序的复杂度,其符号为VG或是M。圈复杂度是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,

即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。独立路径组成的集合称为基本路径集合,独立路径数就是指基本路径集合中路径的数量。

基本路径集合不是唯一的,独立路径数也就不唯一。因此,圈复杂度是最大独立路径数。

1.2 控制流图

圈复杂度的计算一般都是需要根据控制流图来进行计算,控制流图(Control Flow Graph, CFG)是一个过程或程序的抽象表现,代表了一个程序执行过程中会遍历到的所有路径。流图中有3个概念:节点、边、区域。流图中的圆称为节点,一个节点代表一条或多条语句;

流图中的箭头线称为边,代表控制流;由边和节点围成的部分称为区域。

下面是从网上借鉴了一个程序流程图转换成控制流图的示意图,如下所示:

其中,2、3和4、5节点都是对应程序流程图中的两个语句,但它们都是在同一条路径上,所以在流图中只是一个节点。

1.3 圈复杂度计算

圈复杂度计算一般有以下几计算方式,下面根据流图再来看一下圈复杂度计算方式:

1.通过流图中点边进行计算:  V(G) = E−N+2

其中E是流图中边的条数,N是流图中节点数。所以使用这种方式计算,上图的V(G) =11 - 9 +2 = 4

2. 通过流图中判定节点计算: V(G) = P+1

其中P是流图中判定节点的数目。在流图中当一个节点分出两条或多条边指向其他节点时,这个节点就是一个判定节点。上面流图中p为3 所以上图的 V(G) = 3+1 = 4

3. 通过流图中区域计算: V(G) = R

其中R是区域数。计算区域数时不仅包括由边和节点围起来的区域,也包括图外部未被围起来的那个区域。上面流图中R为4 所以上图的V(G) = 4

4. 通过条件表达式计算:

上面的常规计算方法需要画出控制流图有些麻烦,我们可以粗暴一点根据判定条件节点计算法粗略估计,其实也就是上图中的第二个计算方式。

一般圈复杂度中的判定节点是指下面这些条件语句:

条件语句: if语句、while语句(包含do...while...语句)、for语句(包含foreach语句)、switch case语句。

条件表达式(二元或多元):&& 表达式、|| 表达式、三元运算符。

注: 这里强调下圈复杂度是不包括try/catch的,网上凡是带有try/catch的文章,写的都是错的。

我们计算时,正常的顺序代码,圈复杂度加1,然后遇到上面的判定节点就加1。比如现在有段代码,先把圈复杂度初始为1,然后遇到一个if语句就加1,遇到一个for语句就加1 ,那这段代码圈复杂度就是3。

需要注意的是,判定节点计算法对于多分支的 case 结构或多个 if - else 结构,每个case或者 else 都需要统计。


圈复杂度是合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码的判断逻辑复杂,程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系。

二 认知复杂度(Cognitive Complexity, Cogc)

前面介绍了圈复杂度,但是圈复杂度是有一些缺陷的,以下引用自sonar官方白皮书翻译:

Thomas J. McCabe 的圈复杂度长期以来一直是衡量方法控制流复杂度的事实标准。它最初的目的是“识别难以测试或维护的软件模块”[1],但虽然它准确计算了完全覆盖一个方法所需的最少测试用例数,但它并不是一个令人满意的可理解性度量。这是因为具有相同圈复杂度的方法不一定给维护者带来相同的难度,导致测量通过高估某些结构而低估其他结构的“假象”。

同时,圈复杂度不再是全面的。它于 1976 年在 Fortran 环境中制定,不包括现代语言结构,如try/catch和lambdas。最后,因为每个方法的最小圈复杂度分数为 1,所以不可能知道任何具有高聚合圈复杂度的给定类是属于大型的、易于维护的类还是具有复杂控制流的小类。除了类级别之外,人们普遍认为应用程序的圈复杂度分数与其代码行总数相关。换句话说,圈复杂度在方法级别之上几乎没有用处。

简单来说就是,圈复杂度已经有些过时并不能涵盖现有程序语言结构,而且也并不能衡量代码的可理解性,那么有什么方式解决圈复杂度的这些问题呢?

针对这点,我们可以使用认知复杂度:

为了解决这些问题,我们制定了认知复杂度(Cognitive Complexity)用于解决现代语言结构,并在类和应用程序级别产生有意义的值。更重要的是,它脱离了基于数学模型评估代码的实践,因此它可以产生与程序员对理解这些流程所需的心理或认知相对应的控制流评估。

认知复杂度打破了使用数学模型来评估软件可维护性的做法。它使用人类判断来评估应该如何计算结构,并决定应该将什么添加到整个模型中。因此,它产生的方法复杂性分数让程序员觉得比以前的模型更公平的可理解性相对评估。此外,由于认知复杂度

不收取方法的“入门成本”,因此它不仅在方法级别,而且在类和应用程序级别都会产生更公平的相对评估。

如果你需要识别代码的理解难度,那就应该使用认知复杂度。比如下面我引用下sonar官方的一个例子,下面这段代码,圈复杂度都是4,但是我们很明显的可以看出来,两边代码的可理解性是不相同的

我们再来看看如果用认知复杂度,结果是什么

通过这个例子,可以比较明显的看出,相比圈复杂度,认知复杂度更适合我们对代码可理解性的进行量化评估。

我们再来看下认知复杂是怎么计算的,下面引用下认知复杂度基本规则评估方式:

认知复杂度分数根据三个基本规则进行评估:

1. 忽略允许将多个语句易于理解地简写成一个的情况。

2. 在代码线性流程中的每一次中断都增加(+1)(复杂度)。

3. 断流结构嵌套时增加(复杂度)


简单来说,认知复杂度中,多个语句简写为一个语句,复杂度不会增加,如果线性代码逻辑中,逻辑被一些条件语句或条件表达式打断一次,复杂度就会加一,如果打断逻辑的语句是一个嵌套的语句,那么复杂度需要额外再加上嵌套所带来的复杂度。

下面再举个例子说明下:

认知复杂度更多相关内容,可以参考sonar官方白皮书,详见以下链接:

https://www.sonarsource.com/docs/CognitiveComplexity.pdf

三 基本复杂度(Essential Complexity, ev(G))

基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。

将圈复杂度图中的结构化部分简化成一个点,计算简化以后流程图的圈复杂度就是基本复杂度。

四 模块设计复杂度(Module Design Complexity, iv(G))

     模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。

      除了上面介绍的一些复杂度之外,还有些其他复杂度比如设计复杂度,集成复杂度,规范化复杂度等等,就不再展开。

五 复杂度计算工具

上面复杂度计算都有些麻烦,平时如果自己去计算工作量不小,不过目前都已经有专门的工具用来计算这些复杂度,比如Metrics Reloaded,SonarLint等等,这些工具都有相应的idea插件,平时开发时使用这些工具就可以做到代码实时检测分析代码复杂度,可以有效

的帮我们识别代码复杂度。至于使用方法,这里也不再展开了。

下面这个是常见的复杂度标准,以供参考:

六 如何降低复杂度

降低代码复杂度的方式比较多,复杂度主要是条件语句和条件表达式引起的,所以降低复杂度的方法,也就是减少这些语句,一般有以下常用方法:


1.将方法拆分,抽出部分独立逻辑组成小的方法。

2.尽量简化、合并条件表达式,比如合并多个同义的if语句,使用多元运算符代替if else等。

3.减少else,比如可以调整表达式顺序,使用卫语句等等

4.使用lambda代替部分逻辑。lambda虽然也会增加复杂度,但有些场景下会比条件表达式复杂度低

相关文章
|
算法 PyTorch 算法框架/工具
昇腾 msmodelslim w8a8量化代码解析
msmodelslim w8a8量化算法原理和代码解析
1301 5
|
搜索推荐 UED Python
实现一个带有昼夜背景切换的动态时钟:从代码到功能解析
本文介绍了一个使用Python和Tkinter库实现的动态时钟程序,具有昼夜背景切换、指针颜色随机变化及整点和半点报时功能。通过设置不同的背景颜色和随机变换指针颜色,增强视觉吸引力;利用多线程技术确保音频播放不影响主程序运行。该程序结合了Tkinter、Pygame、Pytz等库,提供了一个美观且实用的时间显示工具。欢迎点赞、关注、转发、收藏!
662 94
|
人工智能 文字识别 自然语言处理
保单AI识别技术及代码示例解析
车险保单包含基础信息、车辆信息、人员信息、保险条款及特别约定等关键内容。AI识别技术通过OCR、文档结构化解析和数据校验,实现对保单信息的精准提取。然而,版式多样性、信息复杂性、图像质量和法律术语解析是主要挑战。Python代码示例展示了如何使用PaddleOCR进行保单信息抽取,并提出了定制化训练、版式分析等优化方向。典型应用场景包括智能录入、快速核保、理赔自动化等。未来将向多模态融合、自适应学习和跨区域兼容性发展。
1124 29
|
传感器 监控 Java
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
603 5
|
自然语言处理 搜索推荐 数据安全/隐私保护
鸿蒙登录页面好看的样式设计-HarmonyOS应用开发实战与ArkTS代码解析【HarmonyOS 5.0(Next)】
鸿蒙登录页面设计展示了 HarmonyOS 5.0(Next)的未来美学理念,结合科技与艺术,为用户带来视觉盛宴。该页面使用 ArkTS 开发,支持个性化定制和无缝智能设备连接。代码解析涵盖了声明式 UI、状态管理、事件处理及路由导航等关键概念,帮助开发者快速上手 HarmonyOS 应用开发。通过这段代码,开发者可以了解如何构建交互式界面并实现跨设备协同工作,推动智能生态的发展。
927 10
鸿蒙登录页面好看的样式设计-HarmonyOS应用开发实战与ArkTS代码解析【HarmonyOS 5.0(Next)】
|
SQL Java 数据库连接
如何在 Java 代码中使用 JSqlParser 解析复杂的 SQL 语句?
大家好,我是 V 哥。JSqlParser 是一个用于解析 SQL 语句的 Java 库,可将 SQL 解析为 Java 对象树,支持多种 SQL 类型(如 `SELECT`、`INSERT` 等)。它适用于 SQL 分析、修改、生成和验证等场景。通过 Maven 或 Gradle 安装后,可以方便地在 Java 代码中使用。
4931 11
|
PHP 开发者 容器
PHP命名空间深度解析:避免命名冲突与提升代码组织####
本文深入探讨了PHP中命名空间的概念、用途及最佳实践,揭示其在解决全局命名冲突、提高代码可维护性方面的重要性。通过生动实例和详尽分析,本文将帮助开发者有效利用命名空间来优化大型项目结构,确保代码的清晰与高效。 ####
265 20
|
机器学习/深度学习 存储 人工智能
强化学习与深度强化学习:深入解析与代码实现
本书《强化学习与深度强化学习:深入解析与代码实现》系统地介绍了强化学习的基本概念、经典算法及其在深度学习框架下的应用。从强化学习的基础理论出发,逐步深入到Q学习、SARSA等经典算法,再到DQN、Actor-Critic等深度强化学习方法,结合Python代码示例,帮助读者理解并实践这些先进的算法。书中还探讨了强化学习在无人驾驶、游戏AI等领域的应用及面临的挑战,为读者提供了丰富的理论知识和实战经验。
939 5
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
628 10
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
336 1

推荐镜像

更多
  • DNS