查看原文
其他

代码圈复杂度治理小结

陈胜利(李渔) 阿里开发者 2022-09-10


网上有个段子,说建筑工程师不会轻易答应会给摩天大楼增加一个地下室,但代码开发工程师却经常在干这样的事,并且总有人会对你说“这个需求很简单”。到土里埋个雷,这确实不复杂,但我们往往面临的真实场景其实是“在一片雷区的土里埋一个雷”。而雷区里哪里有雷,任何人都不知道。
回到我们日常的写代码的场景,我们一直在说系统很复杂,那到底什么是系统复杂度呢?最近几个月,公司广目平台(注:是公司内部的代码评价平台)进入大家视野,很多同学开始关注起自己代码力的得分情况。作为团队的稳定性底盘负责人,也经常和大家探讨为什么会因为圈复杂度高而被扣分。那么,怎么才能写的一手可读,可扩展,可维护[注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 = 3public 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~10
清晰
10~20
复杂
20~30
非常复杂
>30
不可读
不可测
非常高


我该怎么做


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, 第一层“我的代码写完了”,第二层“我的代码写好了”,第三层“我的代码能跑了”,第四层“我的代码还在用”


阿里云产品评测—阿里云容器镜像服务 ACR


免费试用体验面向容器镜像、Helm Chart 等符合 OCI 标准的云原生制品安全托管及高效分发平台,发布你的评测更有机会获得千元机械键盘,限量定制礼品。


点击阅读原文查看详情。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存