如何有效的解决代码的圈复杂度

如何,有效,解决,代码,复杂度 · 浏览次数 : 81

小编点评

**圈复杂度** 圈复杂度是指代码中控制流图中节点数量和边数之间的差距,反映了代码的可读性和易维护性。 **计算方法** * **点边计算法**:V(G) = E − N + 2,其中: * E:控制流图中的边数量 * N:控制流图中的节点数量 * **节点判定法**:V (G) = P + 1,其中: * P:判定节点数量 **降低圈复杂度的方法** * **提炼函数**:将高内聚的操作提炼到独立方法中。 * **合并条件**:通过条件分支简化条件分支。 * **多态替代**:使用多态的方式进行处理,降低分支语句的数量。 * **控制标记**:通过控制标记对循环进行处理,减少分支语句的数量。

正文

作者:京东零售 杨学刚

背景介绍

不管小型公司还是大型互联网公司,很多项目债台高筑,新功能开发困难。其中一个很大的原因就是代码复杂,可读性差。Sonar开发团队曾上纲上线的戏称开发人员的7宗罪,其中很关键的一条就是“复杂度”。那复杂度有没有一个明确的衡量标准,我们又如何去解决代码的圈复杂度呢?今天我在这里和大家聊一下。

圈复杂度的计算方法

我们先来看一下圈复杂度与代码质量以及测试和维护成本之间的一个关系。

我们可以看到当圈复杂度,在1-10之间的时候,代码是清晰,结构化的。可测试性比较高,维护成本也比较低。随着圈复杂度的升高,代码的状况开始恶化,当大于30的时候,代码已经逐步变为不可读,维护成本非常高。

点边计算法

那圈复杂度是如何计算的呢,常用的第一种方法叫做点边计算法,它圈复杂度的计算方式 V(G) = E − N + 2,我们用下边图来解释一下这个公式:

其中公式之中的E指的是控制流图中边的数量,N指的是控制流图中的节点数量。这两个图形指的就是控制流图。那我们可以计算一下,第一个控制流图的圈复杂度是:4-4+2=2.

节点判定法

除此之外圈复杂度还有一种更为直观的计算方法,因为圈复杂度实际上体现了“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上1。它的计算公式为:V (G) = P + 1 其中判定节点(P)指的是我们常用的分支语句。例如if语句、while语句、case语句等。

那如何来降低圈复杂度呢?

圈复杂度的常用解决方法

提炼函数

接下来我们重点介绍一些降低圈复杂的方法,我通过工作中常见的代码,来表述一下,如何去降低复杂度,如果你有更好的方法,也欢迎留言跟我交流。在我们的工作中,做业务系统的时候,通过异步消息进行数据传递,是比较常用的一种方式,在我们监听到对端系统的消息的时候,一般会做这几件事情。判断消息是否为空-->转换消息为数据传输对象DTO-->进一步的判断对象的数据是否合法-->进行业务逻辑的处理。这几个典型的步骤,很多童鞋可能用左边图的方式进行处理。这个时候,如果每一个步骤的方法比较复杂的时候,这个总的方法会非常复杂,这个时候,我们可以通过提炼方法的方式,对高内聚的操作,提炼到一个独立的方法中,来分治复杂性。

使用卫语句

我们知道圈复杂度的一个因素就是分支语句多,我们在写业务代码的时候,常见到这样的一种代码,if-then-else的层层嵌套。卫语句的原则是,如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时,立刻返回。下面是一个生产中的场景,如果记账请求落库成功后就进行余额的操作,如果不成功就返回失败结果。因为落库失败是不常见的,所以我们采用卫语句的方式,来减少分支语句。让代码更清晰。

合并条件

经常遇到一种情况,我们对错误的处理,需要返回给调用方,内部的错误码,为了方便快读的定位错误会非常详细,但是对外可能会泛化这种错误码,这个时候我们可以通过合并条件的方式,简化条件分支,来降低圈复杂度。下面是一个生产中的场景,如果记账失败,则对错误结果进行包装处理,并返回给调用方。这个时候我们可以将错误码合并,这里它是合并到map中,然后针对这组错误码统一进行了处理。

通过多态方式替代条件式

在我们开发中,如果是一个平台化的系统,很多时候,有这样的需求。例如:不同的租户、不同的业务甚至不同的订单类型都会有不同的处理流程。 这个时候最简单的方式,就是通过条件分支来进行不同的处理。但是当业务繁多的时候,处理分支会显得混乱,从而导致圈复杂度的升高,这个时候我们通过利用多态的方式,可以有效的降低复杂度。我们看一下下边这段代码,不同的订单类型,使用不同的处理流程,这里他使用了在枚举中实现多态的方式。我们发现,其实他是实现了工厂模式。

替换算法

复杂算法会导致bug可能性的增加及可理解性/可维护性的降低,如果函数对性能要求不高,提倡使用简单明了的算法。这里我引用了重构中的一个例子,我们可以一起看一下。这里传入一个人名的数组,如果数组中包含指定的名称,就立即返回名称。

分解条件式

在面对大块头的代码时,你可以通过提炼方法的方式,将它分解为多个方法。根据每个小块代码的用途,命名新的方法名。对于条件逻辑,将每个分支条件分解成新方法可以突出条件逻辑,并更清楚的表达每个分支的作用。比如下面的例子中,夏季的时候商品的折扣和非夏天的商品折扣,是不同的计算方法。 这个时候,我们可以把两种算法,提炼到两个不同的方法中.

移除控制标记

有时候我们会通过控制标记来对循环进行处理,我们看一下这样的一段经常使用的代码,同一个数组列表中查找罪恶的人,匹配到任意一个罪恶的人后返回。这里found是控制标记,我们可通过下边的方式去掉控制标记,来减少一层循环,达到削减复杂度的效果。

圈复杂度的思辨

那是不是当我们检测到圈复杂度高的时候他就一定复杂呢,下面的代码是一个生产上的例子,他通过传入的MQ的名字,对MQ进行手动的暂停。这个地方实际上是可以通过mq的名称,从spring的容器中,获取bean的。这里的例子主要是让大家看到,虽然,这个分支比较多,但是这种扁平化的结构可读性还是可以的。不过如果它做的不仅仅是一个暂停的操作,而是一个很复杂的操作,这个时候,可能就需要通过提炼方法的方式进行重构。如果提炼方法重构后,这个类还是过长,那就需要我们通过使用多态的特性,利用工厂模式等方式进行进一步的重构。如果一开始我们就通过应用一些复杂的设计模式进行重构,就会存在过度设计的弊端,使代码更不易于理解。

总结

首先介绍了什么是圈复杂度,然后介绍了解决圈复杂度的几种方法。

通过圈复杂度计算的两种方式我们可以看到,圈复杂度的核心是分支语句。那解决问题的核心就集中在如何去减少分支语句。

不过最后我们也看到了,实际上,只是刻板的使用圈复杂度的算法,去度量一个段代码的清晰度,有时候也是不可取的,所以我们在重构系统的时候,可以通过圈复杂度的工具,进行复杂度的统计,然后对复杂度高的代码,具体场景,具体分析。而不能一味的教条。

最后我们通过思维导图来梳理一下:

与如何有效的解决代码的圈复杂度相似的内容:

如何有效的解决代码的圈复杂度

不管小型公司还是大型互联网公司,很多项目债台高筑,新功能开发困难。其中一个很大的原因就是代码复杂,可读性差。那复杂度有没有一个明确的衡量标准,我们又如何去解决代码的圈复杂度呢?本篇文章将详细讲解圈复杂度的计算方式以及常用的解决方法。

如何让Java编译器帮你写代码

本文结合京东监控埋点场景,对解决样板代码的技术选型方案进行分析,给出最终解决方案后,结合理论和实践进一步展开。通过关注文中的技术分析过程和技术场景,读者可收获一种样板代码思想过程和解决思路,并对Java编译器底层有初步了解。

一个基于GPT模型实现的Git Commit信息自动生成工具

每次提交代码的时候,你是否有为如何写Commit Message而迟迟按不下提交的时刻呢?然后,死磨硬泡写了一些并提交后,又被review的小伙伴吐槽了呢?相信很多小伙伴有过这样的经历吧? 趁着最近ChatGPT那么火,就来顺手推荐一个可以用于解决这个问题的VS Code插件:vscode-gpto

C# WinForm控件及其子控件转成图片(支持带滚动条的长截图)

概述(Overview) 参考了网上的分享,感觉都不太理想:1.一个控件内如果包含多个子控件时没有考虑顺序问题;2.超出控件可显示区域时不能长截图,有滚动条会多余截取了滚动条。这个随笔旨在解决这个问题,实现带滚动条时可以长截图,并且给出了在多个子控件的情况下如何控制截图顺序的代码。有用可以点个赞。引

Java静态变量在静态方法内部无法改变值

一、如何解决“Java静态变量在静态方法内部无法改变值”的问题 在Java中,静态变量(也称为类变量)属于类本身,而不是类的任何特定实例。它们可以在没有创建类的实例的情况下访问和修改。如果我们发现在静态方法内部无法改变静态变量的值,这通常是因为我们的代码中有一些逻辑错误或误解。 下面是一个简单的示例

如何扩展及优化CI/CD流水线?

如今应用程序的开发通常由多个开发人员组成的团队完成。每个人或团队在项目中发挥自己的作用,然后我们发现在项目的末尾总是有几段代码需要编译,根据每个人的工作方法,管理这种集成可能会浪费很多时间。持续集成和持续交付/部署(CI/CD)便用来解决该问题,确保发布更新顺利进行,避免不必要的延迟和冲突。 因此为

代码实例解读如何安全发布对象

摘要:在高并发环境下如何安全的发布对象实例。 本文分享自华为云社区《【高并发】如何安全的发布对象(含各种单例代码分析)》,作者:冰 河。 今天,为大家带来一篇有技术含量的文章,那就是在高并发环境下如何安全的发布对象实例。 发布对象:使一个对象能够被当前范围之外的代码所使用对象溢出:是一种错误的发布,

java中SimpleDateFormat解析日期格式的问题

在日常写代码的过程中,我们经常要处理各种格式的日期,常见的日期格式有:“20240601”,“2024-06-01”,“2024-6-1”。如何正确地处理日期格式,尤其是对外接口中参数的日期格式,就很重要了,一个不小心就可能出现意想不到的问题。 举一个我遇到的真实例子:我们提供的对外接口中有一个参数

数据库连接池之c3p0-0.9.1.2,线上偶发APPARENT DEADLOCK,如何解?

# 前言 本篇其实是承接前面两篇的,都是讲定位线上的c3p0数据库连接池,发生连接泄露的问题。 第二篇讲到,可以配置两个参数,来找出是哪里的代码借了连接后没有归还。但是,在我这边的情况是,对于没有归还的连接,借用者的堆栈确实是打印到日志了,但是我在本地模拟的时候,发现其实这些场景是有归还连接的,所以

记一次 .NET 某埋线管理系统 崩溃分析

## 一:背景 ### 1. 讲故事 经常有朋友跟我反馈,说看你的文章就像看天书一样,有没有一些简单入手的dump 让我们先找找感觉,哈哈,今天就给大家带来一篇入门级的案例,这里的入门是从 WinDbg 的角度来阐述的,这个问题如果你通过 记日志,分析代码 的方式,可能真的无法解决,不信的话继续往下