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

回到我们日常的写代码的场景,我们一直在说系统很复杂,那到底什么是系统复杂度呢?最近几个月,蚂蚁代码力平台(注:是蚂蚁的代码评价平台)进入大家视野,很多同学开始关注起自己代码力的得分情况。作为团队的稳定性底盘负责人,也经常和大家探讨为什么会因为圈复杂度高而被扣分。那么,怎么才能写的一手可读,可扩展,可维护[注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, 第一层“我的代码写完了”,第二层“我的代码写好了”,第三层“我的代码能跑了”,第四层“我的代码还在用”

作者 | 陈胜利(李渔)

原文链接

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

代码圈复杂度治理小结相关推荐

  1. 如何降低前端代码圈复杂度?

    作者 | ConardLi 责编 | maozz 出品 | CSDN(ID:CSDNnews) 写程序时时刻记着,这个将来要维护你写的程序的人是一个有严重暴力倾向,并且知道你住在哪里的精神变态者. 导 ...

  2. 利用Java反射机制降低代码圈复杂度

    利用Java反射机制降低代码圈复杂度 在实际的工作中,我遇到了项目里老代码存在圈复杂度过高的问题,在提交代码的时候通不过CI(代码检查)的Lizard复杂度检查,所以迫切需要解决这个问题,运用Java ...

  3. java代码质量 圈复杂度,代码圈复杂度(Cyclomatic Complexity,CC)和Oclint工具 介绍...

    什么是代码圈复杂度 圈复杂度是一种度量程序复杂度的方法,由 Thomas McCabe 于 1976年定义,用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径条数,即合理的预防错误所需测试的最少 ...

  4. 浅析代码圈复杂度及认知复杂度

    Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/cou ...

  5. 如何降低代码圈复杂度

    已剪辑自: https://mp.weixin.qq.com/s?__biz=MzI2MTE4Nzk5MA%3D%3D&mid=2247483685&idx=1&sn=2607 ...

  6. 前端代码质量-圈复杂度原理和实践

    写程序时时刻记着,这个将来要维护你写的程序的人是一个有严重暴力倾向,并且知道你住在哪里的精神变态者. 1. 导读 你们是否也有过下面的想法? 重构一个项目还不如新开发一个项目... 这代码是谁写的,我 ...

  7. 圈复杂度详解以及解决圈复杂度常用的方法

    1.什么是代码圈复杂度? 圈复杂度(Cyclomatic Complexity)是一种代码复杂度的衡量标准,由 Thomas McCabe 于 1976年定义.它可以用来衡量一个模块判定结构的复杂程度 ...

  8. java 圈复杂度_详解圈复杂度

    详解圈复杂度 圈复杂度概念 圈复杂度(Cyclomatic complexity,简写CC)也称为条件复杂度,是一种代码复杂度的衡量标准.由托马斯·J·麦凯布(Thomas J. McCabe, Sr ...

  9. c语言圈复杂度switch,软件度量-圈复杂度

    最近在做ESLint规则配置,重新注意了下complexity规则,之前对该规则不够重视,理解有盲区,于是系统学习,这里mark下. 概念循环复杂度 Cyclomatic complexity也称为条 ...

  10. 追求代码质量: 监视圈复杂度

    http://www.ibm.com/developerworks/cn/java/j-cq03316/ 每位开发人员对代码质量的含义都有着自己的看法,并且大多数人对如何查找编写欠佳的代码也有自己的想 ...

最新文章

  1. Alisql源码编译安装(详细篇)
  2. 一文轻松了解Graph Neural Networks
  3. 【Python】itertools之product函数
  4. 去中心化钱包CoinU诞生 黑客攻不破的铜墙铁壁
  5. linux开机启动rsync,【Linux】rsync同步文件 程序自启动
  6. 《XNA高级编程:Xbox 360和Windows》1-2
  7. mysql optimizer mrr_MySQL优化器功能开关optimizer_switch
  8. K8s上的Go服务怎么扩容、发版更新、回滚、平滑重启?教你用Deployment全搞定!
  9. 开源框架openresty+nginx 实现web应用防火墙(WAF)
  10. pytorch 向量转化为one-hot编码
  11. 【python简洁之道】-----1. 注释规则
  12. jquery ajax修改密码,提交form表单---修改密码 ajax、jQuery
  13. 3D制图软件中怎么设计凸轮?3D设计凸轮教程
  14. YS-F4Pro开发板第四章:开发环境搭建【IAR和KEIL区别】【cube软件】【ST-Link和Jlink】
  15. FPGA之道(6)软件编程思路与FPGA编程思路的变革
  16. html、css做一个带搜索图标的搜索框(方法分享)
  17. 根据pix飞控log文件和photo文件夹建立pos文件,提取pos数据小程序
  18. 网吧电影视频服务器架设完全入门教程(转)
  19. 常用的无线充发射IC芯片
  20. 计算机三级网络技术最全知识点总结三

热门文章

  1. 经济学人The Economist学习(笔记词汇)Day1
  2. 创建git仓库|将本地项目上传到git仓库
  3. python动画精灵_Python小课堂第18课:如何使用Pygame做动画精灵和碰撞检测
  4. 苹果中国全系降价:iphone最高降500元,用户可退差价
  5. 一个k8s集群——跨云服务器部署
  6. Android BLE GATT CONN LMP TIMEOUT 0x22
  7. ImageUndistorted_Disparity
  8. python选取tensor某一维_超简单!pytorch入门教程(一):Tensor
  9. 六万字带你一次性速通python爬虫基础
  10. 一个人的精神结构和他的精神资源