代码圈复杂度治理小结

网上有个段子,说建筑工程师不会轻易答应会给摩天大楼增加一个地下室,但代码开发工程师却经常在干这样的事,并且总有人会对你说“这个需求很简单”。到土里埋个雷,这确实不复杂,但我们往往面临的真实场景其实是“在一片雷区的土里埋一个雷”。而雷区里哪里有雷,任何人都不知道。

回到我们日常的写代码的场景,我们一直在说系统很复杂,那到底什么是系统复杂度呢?最近几个月,蚂蚁代码力平台(注:是蚂蚁的代码评价平台)进入大家视野,很多同学开始关注起自己代码力的得分情况。作为团队的稳定性底盘负责人,也经常和大家探讨为什么会因为圈复杂度高而被扣分。那么,怎么才能写的一手可读,可扩展,可维护[注1]的好代码?本文作者尝试结合在团队内部的实践,分享下过程中心得,希望对大家的代码圈复杂度治理提供微弱的帮助。

什么是圈复杂度

先看看圈复杂度的通用的定义,圈复杂度(Cyclomatic complexity,简写CC)[注2]也称为条件复杂度/循环复杂度,是一种代码复杂度的衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度,其符号为VG。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。说人话,圈复杂度关系到质量同学最少需要设计多少用例才能覆盖你的代码分支。

怎么计算圈复杂度

蚂蚁广目平台给出了比较详细的说明,这里直接引用,网上也可以查到类似内容。

节点判断计算公式为:V (G) = P + 1 注:除了节点判断法,还有其他方法,如点边判断法,这里只选一个用于说明。

其中P为条件节点数,条件节点类型为:

a.条件语句

  • if语句
  • while语句(包含do...while...语句)
  • for语句(包含foreach语句)
  • switch case语句
  • try catch语句

b.条件表达式(二元或多元)

  • && 表达式
  • || 表达式
  • 三元运算符


举例如下(部分代码省略后用xxx代替):

//案例1,圈复杂度V(G) =  1(if) + 1(catch) + 1 = 3
public String myMethod1(){if(xxx){try {//xxx;} catch (IOException e) {//xxx;}}else{xxx;}return xx;
}//案例2,圈复杂度V(G) =  2(if)  + 1(&&) + 1 = 4  
public String myMethod2() {if (xxx) {//xxx;} else {if (xxx && xxx) {//xxx;} else {//xxx;}xx();}return xx;
}

为什么要关注圈复杂度

好了,了解了圈复杂度的定义之后,我们基本可以得出一个结论,圈复杂度大说明程序逻辑复杂,不利于代码的阅读,维护,和后续扩展。如果需要看懂一个圈复杂度高的方法,需要小心翼翼整理所有的分支情况,而改动这类代码更像踏入雷区一样。

下面,我们来看一段代码案例(部分内容已省略)

public XXresult doSave( XXDTO newScriptDTO) {String type = Enums.ScriptType.CUSTOM;Boolean containsTryCatch = StringUtil.contains(content, "try")&& StringUtil.contains(content, "catch");if (StringUtil.isBlank(scriptName)) {baseOperationResult.setMessage("XXX");return baseOperationResult;}if (!scriptName.matches("^[(\\d)|_|a-z|A-Z]+$")) {baseOperationResult.setMessage("XXX");return baseOperationResult;}NewScript tempScript = null;try {tempScript = newScriptManager.findByName(StringUtil.trim(scriptName));} catch (Exception e) {baseOperationResult.setMessage("XXX");return baseOperationResult;}if (StringUtil.isBlank(id)) {if (tempScript != null) {baseOperationResult.setMessage("XXX");return baseOperationResult;}} else {Integer editScriptId = Integer.parseInt(id);if (null != tempScript) {if (!editScriptId.equals(tempScript.getId())) {baseOperationResult.setMessage("XXX");return baseOperationResult;}}}if (!Enums.NewScriptTypeEnum.XX.contains(scriptType)) {baseOperationResult.setMessage("XX");return baseOperationResult;}Boolean needSubtypeMode = true;if (StringUtils.equals(scriptType, Enums.XX.XX)|| StringUtils.equals(scriptType, Enums.XX.PRE)) {needSubtypeMode = false;}NewScript script = new NewScript();script.setScriptType(scriptType);if (StringUtil.isNumeric(status)) {script.setStatus(Integer.parseInt(status));}if (StringUtil.isNotBlank(scriptCategory)) {script.setScriptCategory(ScriptCategory.getByCode(scriptCategory));}String subType = "";if (needSubtypeMode) {if (StringUtil.isBlank(subtypeandtip)) {baseOperationResult.setMessage("XXX");return baseOperationResult;}}if (needSubtypeMode) {List< NewScript> allActiveAndTestRunScripts = newScriptManager.findAllActiveAndTestRunScripts();List< String> allActiveAndTestRunSubTypeList = new ArrayList<>();for (NewScript activeAndTestRunScript : allActiveAndTestRunScripts) {List< String> subTypeListEveryScript = Arrays.asList(Optional.ofNullable(activeAndTestRunScript.getSubType()).orElse(new String()).split(","));for (String subTypeTemp : subTypeListEveryScript) {if (StringUtil.isNotBlank(subTypeTemp)) {allActiveAndTestRunSubTypeList.add(subTypeTemp);}}}try {JSONArray subtypetipsArray = JSON.parseArray(subtypeandtip);if (StringUtil.isBlank(id)) {for (Object object : subtypetipsArray) {JSONObject subtypetipsObject = (JSONObject) object;String subtypeSingle = subtypetipsObject.getString("subtype");if (StringUtil.isBlank(subtypeSingle)) {baseOperationResult.setSuccess(false);return baseOperationResult;}if (CollectionUtils.contains(allActiveAndTestRunSubTypeList.iterator(),subtypeSingle)) {baseOperationResult.setSuccess(false);return baseOperationResult;}}} else {if ("1".equals(status) || "2".equals(status)) {for (Object object : subtypetipsArray) {//省略部分内容XXX;if (StringUtil.isBlank(subtypeSingle)) {baseOperationResult.setSuccess(false);return baseOperationResult;}for (NewScript oldNewScript : allActiveAndTestRunScripts) {if (oldNewScript.getId().equals(Integer.parseInt(id))) {continue;}//省略部分内容XXX;if (CollectionUtils.contains(filtered.iterator(), subtypeSingle)) {baseOperationResult.setSuccess(false);return baseOperationResult;}}}}}for (Object object : subtypetipsArray) {if (1 == script.getStatus() || 2 == script.getStatus()) {SubtypeTips subtypeTips = null;subtypeTips = subtypeTipsManager.findBySubtype(subtypeSingle);if (subtypeTips == null) {subtypeTips = new SubtypeTips();}subtypeTips.setSubtype(subtypeSingle);subtypeTips.setInternalTips(innertips);subtypeTips.setExternalTips(externaltips);subtypeTips.setShareLink(shareLink);subtypeTips.setStatus(1);subtypeTipsManager.save(subtypeTips);}}subType = StringUtil.substring(subType, 0, subType.length() - 1);} catch (Exception e) {baseOperationResult.setSuccess(false);baseOperationResult.setMessage("XXX");return baseOperationResult;}}boolean needCreateTestRunScript = false;if (StringUtils.isNotBlank(id)) {script.setId(Integer.parseInt(id));NewScript orgin = newScriptManager.findById(Integer.parseInt(id));if (null != orgin && 1 == orgin.getStatus() && "1".equals(status)) {if (StringUtil.isNotBlank(orgin.getContent())) {String originContentHash = CodeUtil.getMd5(StringUtil.deleteWhitespace(orgin.getContent()));String contentHash = CodeUtil.getMd5(StringUtil.deleteWhitespace(content));if (!StringUtil.equals(originContentHash, contentHash)) {needCreateTestRunScript = true;}}}} else {script.setSubmitter(user.getLoginName());}Set< String> systemList = new HashSet< String>();if (StringUtil.isNotBlank(systems)) {String[] systemArray = systems.split(",");for (int i = 0; i < systemArray.length; i++) {systemList.add(systemArray[i]);}}if (needCreateTestRunScript) {if (needSubtypeMode) {content = replaceContent(content, subType);String testScriptSubType = "";List< String> subTypeList = Arrays.asList(StringUtil.split(subType, ","));for (int i = 0; i < subTypeList.size(); i++) {testScriptSubType += this.UPDATE_SCRIPT + subTypeList.get(i);if (i != subTypeList.size() - 1) {testScriptSubType += ",";}}subType = testScriptSubType;}scriptName = this.UPDATE_SCRIPT + scriptName;NewScript oldUpdateScript = newScriptManager.findByName(scriptName);if (null != oldUpdateScript)script.setId(oldUpdateScript.getId());else {script.setId(null);}baseOperationResult.setNeedAudit(true);}if (StringUtil.isBlank(fileSuffix)) {//如果全空的话 默认全扫script.setSuffix(".*");} else {script.setSuffix(fileSuffix);}script.setName(scriptName);if (StringUtil.equals(allPath, "Y")) {script.setAllPath("Y");} else {script.setAllPath("");}script.setEnvTag(tenantScope);script.setNeedAutoScan(needAutoScan);if (StringUtil.isNotBlank(scopes)) {for (String each : StringUtil.split(scopes, ",")) {each = StringUtil.replace(each, " ", "");script.addScope(each);}}if (StringUtil.isNotBlank(content)) {BaseOperationResult preLoadResult = syntaxCheck(script);if (!preLoadResult.isSuccess()) {baseOperationResult.setMessage(preLoadResult.getMessage());return baseOperationResult;}}if (StringUtil.contains(content, "new Bug")) {baseOperationResult.setSuccess(false);return baseOperationResult;}try {Result< NewScript> result = newScriptManager.saveCustomScript(script);if (result.isSuccess()) {if (EnvUtil.isProdEnv() && EnvUtil.isLinux()) {if (!needCreateTestRunScript) {//省略部分内容XX} else {//省略部分内容XX}}Boolean hasOldScript = processOldEngineRule(scriptName);if (containsTryCatch) {if (hasOldScript) {//省略部分内容XX} else {//省略部分内容XX}} else {if (hasOldScript) {baseOperationResult.setMessage("XXX");} else {baseOperationResult.setMessage("保存成功!");}}baseOperationResult.setId(script.getId());processTenantRelation(script.getId(), tenantIdList, user.getLoginName());if (!needCreateTestRunScript && needSubtypeMode&& (StringUtil.equals(Enums.XX.COMMON, script.getScriptType())|| (StringUtil.equals(Enums.XX.SCRIPT,script.getScriptType())))) {JSONArray subtypetipsArray = JSON.parseArray(subtypeandtip);for (Object object : subtypetipsArray) {//省略部分内容XX}}} else {baseOperationResult.setSuccess(false);return baseOperationResult;}} catch (Exception e) {baseOperationResult.setMessage("XX");}return baseOperationResult;}

原代码大概400行以上,复杂度69,憋了一口长气才读完。如果让你来接手这段代码,是不是感觉很头疼?需要梳理里面各种分支逻辑,弄清楚主干脉络。

那么什么样的代码才容易读,容易上手呢?一般业界认为代码可读性,可测试,维护成本和圈复杂度有很大关系,具体如下:

我该怎么做

1.【知己知彼,了解自己代码复杂度】这个比较简单,有以下几种方式:

a.自己数下判定节点(if while for catch case and or等)大概就知道圈复杂度是多大了,参考上面怎么计算圈复杂度章节。

b.在蚂蚁内部使用的广目平台,也可以查看到新提交commit记录里,哪些方法圈复杂度比较高。

c.在代码提交之前,自己用idea小插件(Metrics Reloaded插件),一次性扫描自己负责的系统所有方法的复杂度。

红色部分标识圈复杂度,数字越大复杂度越高。

2.【对症下药,降低复杂度】网上有很多方法,我总结了下,大概有以下几种

方法一:抽取出独立逻辑的子方法,把复杂逻辑拆分成几个独立模块,再去读代码,就会感觉清晰很多。以上面举例的复杂度69的方法为例,我们做了如下的方法拆分,是不是感觉清晰了很多?

public XXresult doSave( NewScriptDTO newScriptDTO) {//0.构造结果XXresult result=new XXresult() ;try{//1.脚本名检查scriptNameCheck(newScriptDTO); //2.脚本加载loadScript(newScriptDTO); //3.脚本保存saveScript(newScriptDTO); }catch(XXException e){result.setSuccess(false)result.setMessage("XXX");return result;}catch(Exception e){result.setSuccess(false)result.setMessage("XXX");return result;}//操作完成result.setSuccess(true)result.setMessage("XXX");return result;}
/**检查脚本名*/
private void scriptNameCheck(NewScriptDTO newScriptDTO){xxx
}
/**加载脚本*/
private void loadScript(NewScriptDTO newScriptDTO){xxx
}
/**保存脚本*/
private void saveScript(NewScriptDTO newScriptDTO){xxx
}

方法二:优化逻辑判断,通过提取频繁出现的条件, 或者调整判断顺序等方式达到简化代码目的。

/ 案例1,抽取频繁出现的条件a/
//修改前
if (条件1)
{if (条件a){// 执行a逻辑}
}
else if(条件2)
{if (条件a){// 执行b逻辑}
}
if (条件a)
{// 执行c逻辑
}
//修改后
if (条件a)
{if (条件1){// 执行a逻辑}else if(条件2){// 执行b逻辑}    // 执行c逻辑
}
/ 案例2,优化逻辑判断顺序/
//修改前
if((条件1 && 条件2)|| !条件1){return true;
}else{return false;
}
//修改后
if(条件1 && !条件2){return false;
}
return true;

方法三:适当使用java新特性,降低大量的if判断。下面是来自团队一淏同学的提供的优化案例

//修改前List list = XXX;if (CollectionUtils.isEmpty(list)) {for (XX item : list) {if (item==null){return;}else{// 逻辑a}}//修改后List list = XX;list = Optional.ofNullable(list).orElse(new ArrayList<>());list.stream().filter(Objects::nonNull).forEach(item->{//逻辑a});}

当然,只要用心钻研,降低复杂度还有很多方法,这里不一一列举了。总结下思路:

  1. 一个方法/类不要写大段大段的代码,把内容封装在逻辑独立的子类和子方法里。
  2. 采用有意义的类名,方法名,让使用者见名思意,易于上手。
  3. 逻辑表达上,优化判断逻辑成最简形式。
  4. 适当使用编程技巧,合并判断方式。

结语

作为蚂蚁工程师的我们,开发代码也应该像创作一个艺术品,深思熟虑,精雕细刻,经过产品的不断升级迭代,仍然能够保持顽强的生命力,就像代码四层境界[注3]里面说的第四层,经过了时间历练“我的代码还在用”。

引用:

[注1]对代码的领悟之-高质量代码有三要素:可读性、可维护性、可扩展性 :https://wenku.baidu.com/view/ce7e54e60f22590102020740be1e650e52eacff5.html

[注2]详解圈复杂度:https://baike.baidu.com/item/%E5%9C%88%E5%A4%8D%E6%9D%82%E5%BA%A6/828737

[注3]代码的四层境界:

https://www.sohu.com/a/238434622_185201, 第一层“我的代码写完了”,第二层“我的代码写好了”,第三层“我的代码能跑了”,第四层“我的代码还在用”

作者 | 陈胜利(李渔)

原文链接

本文为阿里云原创内容,未经允许不得转载。

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

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

相关文章

MSE 治理中心重磅升级-流量治理、数据库治理、同 AZ 优先

本次 MSE 治理中心在限流降级、数据库治理及同 AZ 优先方面进行了重磅升级&#xff0c;对微服务治理的弹性、依赖中间件的稳定性及流量调度的性能进行全面增强&#xff0c;致力于打造云原生时代的微服务治理平台。 前情回顾 在介绍升级能力之前&#xff0c;先简要回顾 MSE 产…

基于阿里云 Serverless 快速部署 Function 的极致体验

1.Serverless 前世今生 1.1 Serverless 背景介绍 云计算的不断发展&#xff0c;涌现出很多改变传统IT架构和运维方式的新技术&#xff0c;而以虚拟机、容器、微服务为代表的技术更是在各个层面不断提升云服务的技术能力&#xff0c;它们将应用和环境中很多通用能力变成了一种…

性能提升1倍,成本直降50%!基于龙蜥指令加速的下一代云原生网关

​ 技术背景 网络信息传输的可靠性、机密性和完整性要求日渐提升&#xff0c;HTTPS 协议已经广泛应用。HTTPS 的 SSL/TLS 协议涉及加解密、校验、签名等密码学计算&#xff0c;消耗较多 CPU 计算资源。因此 CPU 硬件厂商推出过多种加速卸载方案&#xff0c;如 AES-NI、QAT、KA…

TiDB、OceanBase、PolarDB-X、CockroachDB 二级索引写入性能测评

为什么要做这个测试 二级索引是关系型数据库相较于NoSQL数据库的一个关键差异。二级索引必须是强一致的&#xff0c;因此索引的写入需要与主键的写入放在一个事务当中&#xff0c;事务的性能是二级索引性能的基础。 目前市面上的分布式数据库中&#xff0c;从使用体验的角度看…

EMQX + PolarDB-X 一站式 IoT 数据解决方案

本文整理自 EMQX 产品经理李国伟&#xff0c;在PolarDB开源社区中关于EMQX与PolarDB-X构建一站式IoT数据解决方案的分享。本篇内容主要分为四个部分&#xff1a; 1. IoT数据特性 2. EMQX介绍 3. EMQX与PolarDB-X集成 4. EMQXPolarDB-X方案DEMO 一、IoT数据特性 物联网应用场景…

阿里 Seata 新版本终于解决了 TCC 模式的幂等、悬挂和空回滚问题

大家好&#xff0c;我是君哥。 今天来聊一聊阿里巴巴 Seata 新版本&#xff08;1.5.1&#xff09;是怎么解决 TCC 模式下的幂等、悬挂和空回滚问题的。 TCC 回顾 TCC 模式是最经典的分布式事务解决方案&#xff0c;它将分布式事务分为两个阶段来执行&#xff0c;try 阶段对每…

10分钟部署一个别人可以访问的在线网站(文末有礼

你是否幻想过拥有自己的个人网站&#xff1f;但是不会编程&#xff0c;没有任何网站搭建经验&#xff0c;搭建的时候也不知道怎么去选择系统…… 等等这一系列疑惑让大部分人还没开始就选择放弃&#xff0c;本期教大家用一个最简单的方式&#xff0c;在10分钟内搭建一个线上的…

菜鸟 CPaaS 平台微服务治理实践

背景 CPaaS&#xff08;cainiao platform as a service&#xff09;是以公有云为基座&#xff0c;结合先进的云原生理建设的企业级 DevOps 的 PaaS 平台&#xff0c;CPaaS 主要目前主要支持的场景&#xff1a;菜鸟生态的云上研发运维、菜鸟公有云 SaaS 化的能力透出、菜鸟商业…

RocketMQ 消息集成:多类型业务消息-普通消息

引言 Apache RocketMQ 诞生至今&#xff0c;历经十余年大规模业务稳定性打磨&#xff0c;服务了 100% 阿里集团内部业务以及阿里云数以万计的企业客户。作为金融级可靠的业务消息方案&#xff0c;RocketMQ 从创建之初就一直专注于业务集成领域的异步通信能力构建。本篇将从业务…

【总结】字符串匹配: KMP 和 拓展KMP

比起ac自动机,kmp就一个next数组,理解了如何初始化next后就可以搞一些模板题了,下面是还不错的学习资料,清晰易懂,自己用的模板也来自它: http://chaoswork.com/blog/2011/06/14/kmp%E7%AE%97%E6%B3%95%E5%B0%8F%E7%BB%93/ kmp模板 next[0]-1;j-1; for(i0;i<m;) {while(j>…

最小生成树(普利姆算法、克鲁斯卡尔算法)

给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树. 求最小生成树的算法 (1) 克鲁斯卡尔算法 图的存贮结构采用边集数组,且权值相等的边在数组中排列次序可以是任意的.该方法对于边相对比较多的不是很实用,浪费时间. (2) 普里姆算法 图…

《数字化与碳中和(园区篇)》报告正式发布,助力加快推进国家“双碳”战略实施

2021年10月&#xff0c;国务院印发《2030年前碳达峰行动方案》&#xff0c;明确提出要建设绿色低碳园区&#xff0c;并选择100个具有典型代表性的城市和园区开展碳达峰试点建设&#xff0c;在政策、资金、技术等方面对试点城市和园区给予支持。此后&#xff0c;碳达峰、碳中和正…

基于开放共享的自主研发—MaxCompute 持续增强生态与开放性建设

MaxCompute产品与生态架构 MaxCompute是一个具有先进架构的Serverless云数据仓库&#xff0c;自从商业化后&#xff0c;使用的用户涉及各个行业的头部客户。在生态上需要支持主流的开源产品以及阿里云云产品。其主要包括以下几个方面&#xff1a; 数据接入生态。目前官方提供…

构建数据中台的组织架构

一、中台是一种企业架构 1.TOGAF企业架构标准 TOGAF是一套企业架构标准。企业架构是指整个公司或企业的软件和其他技术的整体观点和方法。企业架构又细分为业务架构、应用架构、数据架构、技术架构几个方向。 其中业务架构的定义是“定义业务战略和组织&#xff0c;关键业务…

源于加速,不止加速——10年沉淀,破局改变

20余年技术&#xff0c;面临破局。CDN(Content Delivery Network&#xff0c;内容分发网络) 是一个超大规模的分布式系统&#xff0c;为互联网各类App和Web站点提供动 / 静态内容、实时流媒体加速以及网络安全防护等能力。在线购物、直播、音乐、游戏、社交等等一切&#xff0c…

5分钟让你在大火的多模态领域权威榜单VQA上超越人类

ModelScope上开源了达摩院众多业界最强多模态模型&#xff0c;其中就有首超人类的多模态预训练视觉问答模型mPLUG&#xff0c;小编激动的搓搓小手&#xff0c;迫不及待的体验了一下。 一探&#xff1a;浅草才能没马蹄 市面上有好多号称“用户上手简单”&#xff0c;“一步到位…

私有化输出的服务网格我们是这样做的

介绍 微服务开发的问题 微服务架构下我们在开发中遇到的常见的问题有以下 4 个&#xff1a; 多语言问题&#xff1a;有多种编程语言&#xff0c;node.js, JAVA, GoLang…微服务需要为每种语言都维护一种中间件 SDK升级推动难&#xff1a;SDK 升级需要推动业务应用进行代码修…

技术解读:Dragonfly 基于 P2P 的智能镜像加速系统

背景 网络下载 提起网络下载领域&#xff0c;你应该首先会想到基于 TCP/IP 协议簇的 C/S 模式。这种模式希望每一个客户机都与服务器建立 TCP 连接&#xff0c;服务器轮询监听 TCP 连接并依次响应&#xff0c;如下图&#xff1a; 上世纪末期&#xff0c;基于 C/S 模式的思想&…

Kruise Rollout:灵活可插拔的渐进式发布框架

前言 Kruise Rollout 是 OpenKruise 社区开源的渐进式交付框架。Kruise Rollout 支持配合流量和实例灰度的金丝雀发布、蓝绿发布、A/B Testing 发布&#xff0c;以及发布过程能够基于 Prometheus Metrics 指标自动化分批与暂停&#xff0c;并提供旁路的无感对接、兼容已有的多…

最小生成树的Prime算法的思想

Prime算法的核心步骤是&#xff1a;在带权连通图中V是包含所有顶点的集合&#xff0c; U已经在最小生成树中的节点&#xff0c;从图中任意某一顶点v开始&#xff0c;此时集合U{v}&#xff0c;重复执行下述操作&#xff1a;在所有u∈U,w∈V-U的边(u,w)∈E中找到一条权值最小的边…