Redian新闻
>
代码圈复杂度治理小结

代码圈复杂度治理小结

科技


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


点击阅读原文查看详情。

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
我拿什么奉献给你(5)我给老爸洗澡刘亚东:用互联网思维规范互联网治理是什么引领你的旅行?中秋节旅游分享小结。圆桌会丨帝国边疆治理的多样遗产这个星座心里想的很简单,脑子里思考的却很复杂FA18# 中间件稳定性治理内容提点“名校父母”的“鸡娃号”该如何治理移动应用架构治理初探:从依赖分析与 Android 应用的生命周期说起减少前列腺癌过度治疗,新模式能有用吗?刚刚!澳洲总理对移民政策定调!宣布重大变化!移民部长:澳洲移民要“重置”!提出新计划,修复杂乱签证系统!放弃上北大立志终身下乡搞扶贫实现城市治理一网统管,必须这 4 个关键技术县官是如何治理一个县城的BCG基业长青系列四:法治为纲、德治为魂的家族治理贝壳找房流量分发数据回收与治理演进之路电诈治理关键在“共治”安倍晋三不治身亡:“最长任职首相”的复杂遗产Mackinac 岛丁香季带完这只学霸狗,身为学渣妈的我心情很复杂安倍保镖集体切腹自尽?被刺杀与邪教有关?这事越来越复杂了……数据驱动创新,健全科学数据治理机制刻不容缓数据治理:企业数据管理制度怎么写?田七、田七粉、田七花、田七菜FA17# 缓存设计治理点梳理真的好用!这些值得一买再买的衣物护理小神器,可以唤醒你的衣橱!平台治理谣言和保障言论自由,有可能同时实现吗?【报告】中国企业低代码/无代码产品应用与实践研究——构建数字化作业体系的变速齿轮 | 甲子光年智库肃清司马南孔庆东之流才是网络生态治理的最大成就.......防控体系的重要迭代(二)微观与宏观的小结与心得《代码英雄》第五季(2):写代码的地方 | Linux 中国以全栈全功能解决方案,应对多样工具复杂环境DevOps落地难题如何避免“扇贝跑了”闹剧再现?《沪深300公司治理排行榜》重磅发布No.174# 中间件演进和稳定性治理实践格格不入的威廉姆斯学院WAIC金融科技与数据要素论坛:业界大咖共论隐私计算助力金融数据治理
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。