开发者社区> 开发者小助手_LS> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

代码圈复杂度治理小结

简介: 我们一直在说系统很复杂,那到底什么是系统复杂度呢?作为团队的稳定性底盘负责人,也经常和大家探讨为什么会因为圈复杂度高而被扣分。那么,怎么才能写的一手可读,可扩展,可维护的好代码?本文作者尝试结合在团队内部的实践,分享下过程中心得。
+关注继续查看

image.png

作者 | 陈胜利(李渔)
来源 | 阿里开发者公众号

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

回到我们日常的写代码的场景,我们一直在说系统很复杂,那到底什么是系统复杂度呢?最近几个月,蚂蚁代码力平台(注:是蚂蚁的代码评价平台)进入大家视野,很多同学开始关注起自己代码力的得分情况。作为团队的稳定性底盘负责人,也经常和大家探讨为什么会因为圈复杂度高而被扣分。那么,怎么才能写的一手可读,可扩展,可维护[注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,憋了一口长气才读完。如果让你来接手这段代码,是不是感觉很头疼?需要梳理里面各种分支逻辑,弄清楚主干脉络。

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

image.png


我该怎么做

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

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

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

image.png

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

image.png

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

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 标准的云原生制品安全托管及高效分发平台,发布你的评测更有机会获得千元机械键盘,限量定制礼品。

点击这里,查看详情。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
时间复杂度
在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
66 0
时间复杂度分析
一般情况下,一秒之内相应时间复杂度的算法能解决的数据规模 时间复杂度实验 如何正确的判断算法的时间复杂度 时间复杂度实验:每次将数据规模提高两倍,看时间的变化,以此来验证算法的时间复杂度 以下为代码实例: 测试算法,分别为二分查找O(logN),查找最大值(O(N)),归并排序(O(NlogN)) package com.
1049 0
sublime下让代码居中
sublime在默认情况下当屏幕写满后只能在底端进行输入,对于我这种强迫症患者来说总想着让代码居中显示,在自己查阅相关sublime配置后进行改动。 点击:preference → setting,进入设置界面: 设置界面都是以代码形式呈现,并没有图形界面: 左侧的Default是sublime...
651 0
时间复杂度讲解
58大神讲解时间复杂度 补充: 当然,还要考虑一个函数需要遍历这个集合几次。。。例如:一个循环找最大数的算法是O(N),那么找最大的三个数的算法应该是O(3N)而不能是O(N)...这样。。
700 0
Git 管理本地代码【转】
转自:http://www.cnblogs.com/JessonChan/archive/2011/03/16/1986570.html   以前用SVN,不过没有用出感情来;倒是用出不少怨恨:由于没有很好的备份(我当然要负责的),我所有的代码全没有了~就连我写的锐捷认证的Birl的源码也是我最后从以前的地方重新组合的。
772 0
软考 递归式时间复杂度计算详解
递归算法的时间复杂度分析 在算法分析中,当一个算法中包含递归调用时,其时间复杂度的分析会转化为一个递归方程求解。实际上,这个问题是数学上求解渐近阶的问题,而递归方程的形式多种多样,其求解方法也是不一而足,比较常用的有以下四种方法: 方法一:代换法 代换法主要需要以下两个步骤 1、  猜答案,不需要完全猜出来,不需要知道常熟系数的准确值,而只需要猜出它的形式,比如猜一个递归式的时间复杂度大概是O(n2),即它的运行时间应该是一个常熟乘以n2,可能还会有一些低阶项。
958 0
圈复杂度评价及工具
转载请注明出处:http://blog.csdn.net/horkychen 圈复杂度用来评价代码复杂度,以函数为单位,数值越大表示代码的逻辑分支越多,理解起来也更复杂。
869 0
429
文章
1053
问答
来源圈子
更多
+ 订阅
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载