Code Smell 重构你的日常代码-圈复杂度高多层嵌套

简介: 圈复杂度(Cyclomatic complexity)[1]是一种代码复杂度的衡量标准,在1976年由Thomas J. McCabe, Sr. 提出。条件分支越多,圈复杂度越高,测试越难覆盖,也越难维护。随着业务的不断演进,代码的不断新增与调整,如果只在原逻辑下加入自己的新逻辑,就会长出一个超高嵌套的“气功波”代码。

作者 | 聂晓龙(率鸽)

来源 | 阿里开发者公众号



前言

圈复杂度(Cyclomatic complexity)[1]是一种代码复杂度的衡量标准,在1976年由Thomas J. McCabe, Sr. 提出。条件分支越多,圈复杂度越高,测试越难覆盖,也越难维护。随着业务的不断演进,代码的不断新增与调整,如果只在原逻辑下加入自己的新逻辑,就会长出一个超高嵌套的“气功波”代码。


在我们的祖传代码中,“气功波”式代码不占少数。新增一个条件分支成本是相对低的,它可以让你在不了解原逻辑情况下,完成自己的逻辑。但会持续对系统产生负债,直到有一天,我们真的完全不知道修改的这一行代码,到底影响到了哪些~




Bad Smell

在进行一项业务需求时,接触到了这段祖传代码,通过非常高的嵌套,取其中一项值。由于数据结构非常复杂,为了保证代码的健壮性,原作者写了非常多条件判断,形成了这样一段超高复杂性的“气功波”代码。

/**
 * 解析工单ACTION数据中的【完结原因】
 * @param caseId 工单ID
 * @return
 */
private String queryResolveAction(Long caseId) {
    ActionQueryBizDTO actionQueryBizDTO = new ActionQueryBizDTO();
    actionQueryBizDTO.setBizId(caseId);   //工单id
    actionQueryBizDTO.setDataSource(1);
    ActionQueryDTO actionQueryDTO = new ActionQueryDTO();
    actionQueryDTO.setBizDTOs(Lists.newArrayList(actionQueryBizDTO));
    Result<PageWithData<ActionDTO>> result = ltppActionQueryService.queryActions(actionQueryDTO);
    log.info("query action results:{}", JSON.toJSONString(result));
    if (result.isSuccess() && result.getData() != null) {
        if (CollectionUtils.isNotEmpty(result.getData().getData())) {
            for (ActionDTO actionDTO : result.getData().getData()) {
                if (ACTION_COMPLETE_CODE.equals(actionDTO.getActionCode())) {
                    JSONObject memoObject = JSON.parseObject(actionDTO.getMemo());
                    JSONArray actionKeyMemoArray = memoObject.getJSONArray("actionKeyMemo");
                    for (Object actionKey : actionKeyMemoArray) {
                        Map<String, Object> actionKeyMap = (Map<String, Object>)actionKey;
                        if (MapUtils.isNotEmpty(actionKeyMap) && COMPLETE_REASON.equals(actionKeyMap.get("key"))) {
                            return String.valueOf(actionKeyMap.get("value"));
                        }
                    }
                }
            }
        }
    }
    log.warn("cannot find action given case id {}, the result is {}", caseId, JSON.toJSONString(result));
    return null;
}



重构思路

1.卫语句返回,减少嵌套层级

卫语句(guard clauses)[2]是一种改善嵌套代码的优化方案,将某些要害(guard)条件优先作判断,从而简化程序的流程走向。

public static String getCaseQuestionTitle(CaseTaskRelatedDO caseTask){
    Map<String, Object> extAttrs = caseTask.getExtAttrs();
    if(extAttrs == null || extAttrs.isEmpty()){
        return null;
    }
    JSONObject xform = JSON.parseObject(String.valueOf(extAttrs.get("xform")));
    if(xform == null){
        return null;
    }
    JSONObject body = xform.getJSONObject("body");
    if(body == null){
        return null;
    }
    return body.getString("question_title");
}


2.函数功能收敛,单一职责原则

单一职责原则(Single responsibility principle)[3]强调一个类应该只有一个发生变化的原因,只负责一处职责,由Robert C. Martin首次在 Agile Software Development [4]中提出,并成为面向对象五大设计原则之一。

/**
 * 查询工单Action信息
 * K,V -> ACTION_CODE,ACTION
 * @param caseId
 * @return
 */
private Map<Integer, ActionDTO> queryCaseActionMap(Long caseId){
    ActionQueryBizDTO actionQueryBizDTO = new ActionQueryBizDTO();
    actionQueryBizDTO.setBizId(caseId);
    ActionQueryDTO actionQueryDTO = new ActionQueryDTO();
    actionQueryDTO.setBizDTOs(Lists.newArrayList(actionQueryBizDTO));
    Result<PageWithData<ActionDTO>> result = ltppActionQueryService.queryActions(actionQueryDTO);
    log.info("query action results:{}", JSON.toJSONString(result));
    if(noActionResult(result)){
        return null;
    }
    List<ActionDTO> actionList = result.getData().getData();
    return actionList.stream().collect(Collectors.toMap(ActionDTO::getActionCode, action -> action));
}


3.复杂逻辑抽象,业务语义显性化

Programs are meant to be ready by humans and only icidentally for computers to execute.
-- Donald Ervin Knuth 人工智能之父
译:代码是用来让人读的,只是顺便让机器执行而已。

同样的功能语句,或许转化成汇编后是同样的代码,但对于阅读者而言,不同的表述形式,对于理解成本会有非常大的不同。

/**
 * 工单无ACTION数据
 * @param result
 * @return
 */
private boolean noActionResult(Result<PageWithData<ActionDTO>> result){
    if(result == null){
        return true;
    }
    if(!result.isSuccess()){
        return true;
    }
    if(result.getData() == null){
        return true;
    }
    if(CollectionUtils.isEmpty(result.getData().getData())){
        return true;
    }
    return false;
}


4.关注点分离,抽象解析器模型

关注点分离(Separation of concerns)[5]是将计算机程序分隔为不同部分以便分块聚焦与处理的一种设计原则。这个概念最早在1974年,Dijkstra Edsger在他的文章 On the role of scientific thought [6]中提出的。分离关注点使得解决特定领域问题的程式码从业务逻辑中独立出来,聚焦问题越小复杂程度越低,问题越易解决。

/**
 * 工单解析工具类
 * @author niexiaolong
 * @date 2022/8/24
 */
public class CaseParser {
    /**
     * 解析工单「完结」状态结论
     * @param actionDTO 工单状态集
     * @return 「完结」结论
     */
    private static String parseCompleteConsequence(ActionDTO actionDTO){
        JSONObject action = JSON.parseObject(actionDTO.getMemo());
        if(action == null){
            return null;
        }
        JSONArray actionKeyArray = action.getJSONArray(ACTION_KEY_MEMO);
        if(actionKeyArray == null || actionKeyArray.isEmpty()){
            return null;
        }
        for (int i=0; i<actionKeyArray.size(); i++){
            JSONObject actionKey = actionKeyArray.getJSONObject(i);
            if(actionKey != null && actionDataKey.equals(actionKey.getString(CaseCodeConstant.COMPLETED_DESC_CODE))) {
                return actionKey.getString(ACTION_VALUE);
            }
        }
        return null;
    }
}


5.业务逻辑统一,抽象层次一致性

抽象层次一致性原则(Single Level of Abstration Principle)[7]是 ThoughtWorks 的总监级咨询师 Neal Ford 在 The Productive Programmer [8]一书中提出来的概念。SLAP 强调每个方法中的所有代码都处于同一级抽象层次。如果高层次抽象和底层细节杂糅在一起,就会显得代码凌乱,难以理解,从而造成复杂性。

public List<XSpaceCaseDTO> queryCaseList(String aliId, int currentPage, int pageSize) {
    // 从xspace获取工单列表信息
    List<CaseTaskRelatedDO> caseTaskInfoList = queryCaseListFromXspace(aliId, currentPage, pageSize);
    // 获取每个工单的状态详情
    Map<Long, CaseActionInfo> caseId2ActionInfoMap = queryCaseId2ActionMap(caseTaskRelatedList);
    // 组装工单数据信息
    List<XSpaceCaseDTO> xSpaceCaseList = caseTaskConvertor.convert(caseTaskInfoList, caseId2ActionInfoMap);
    return xSpaceCaseList;
}



Good Smell

最终我们重构后的代码主体逻辑如下,保证程序健壮性的同时,对不同的职责领域进行划分,保持代码的可读性与可维护性,拯救我们的祖传代码~

private CaseActionInfo queryResolveAction(Long caseId) {
    // 获取工单状态集合
    Map<Integer, ActionDTO> actionMap = queryCaseActionMap(caseId);
    if(actionMap == null){
        return null;
    }
    // 优先判断「完结」状态
    if(actionMap.containsKey(CaseCodeConstant.COMPLETE_ACTION_CODE)){
        ActionDTO completeAction = actionMap.get(CaseCodeConstant.COMPLETE_ACTION_CODE);
        String completeConsequence = CaseParseUtils.getCompleteConsequence(completeAction);
        return buildCaseActionInfo(CaseCodeConstant.CASE_COMPLETED, completeAction.getOperatorNick(), completeAction.getGmtModified(), completeConsequence);
    }
    // 其次判断「联系中」状态
    if(actionMap.containsKey(CaseCodeConstant.CONTACTED_ACTION_CODE)){
        ActionDTO contactAction = actionMap.get(CaseCodeConstant.CONTACTED_ACTION_CODE);
        String contactConsequence = CaseParseUtils.getContactedConsequence(contactAction);
        return buildCaseActionInfo(CaseCodeConstant.CASE_CONTACTED, contactAction.getOperatorNick(), contactAction.getGmtModified(), contactConsequence);
    }
    return CaseActionInfo.emptyAction;
}



Smell Battle

我们来看最终的代码效果对比。代码重构不需要单独挑一个复杂的模块,挑一个完整的时间,重构应该在日常开发当中,在我们的编码习惯当中。

image.png


参考链接:

[1]https://baike.baidu.com/item/圈复杂度/828737

[2]https://deviq.com/design-patterns/guard-clause[3]https://deviq.com/design-patterns/guard-clause[4]https://baike.baidu.com/item/敏捷软件开发:原则、模式与实践/2326384[5]https://baike.baidu.com/item/关注点分离/7515217[6]https://www.cs.utexas.edu/users/EWD/transcriptions/EWD04xx/EWD447.html
[7]https://www.techyourchance.com/single-level-of-abstraction-principle/[8]https://nealford.com/books/productiveprogrammer


相关文章
|
消息中间件 网络协议 前端开发
殷浩详解DDD:如何避免写流水账代码?
在日常工作中我观察到,面对老系统重构和迁移场景,有大量代码属于流水账代码,通常能看到开发在对外的API接口里直接写业务逻辑代码,或者在一个服务里大量的堆接口,导致业务逻辑实际无法收敛,接口复用性比较差。所以本文主要想系统性的解释一下如何通过DDD的重构,将原有的流水账代码改造为逻辑清晰、职责分明的模块。
殷浩详解DDD:如何避免写流水账代码?
|
12月前
|
消息中间件 存储 Java
吃透 RocketMQ 消息中间件,看这篇就够了!
本文详细介绍 RocketMQ 的五大要点、核心特性及应用场景,涵盖高并发业务场景下的消息中间件关键知识点。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
吃透 RocketMQ 消息中间件,看这篇就够了!
|
Cloud Native 安全 Java
代码圈复杂度治理小结
我们一直在说系统很复杂,那到底什么是系统复杂度呢?作为团队的稳定性底盘负责人,也经常和大家探讨为什么会因为圈复杂度高而被扣分。那么,怎么才能写的一手可读,可扩展,可维护的好代码?本文作者尝试结合在团队内部的实践,分享下过程中心得。
代码圈复杂度治理小结
|
设计模式 IDE Java
谈谈过度设计:因噎废食的陷阱
写软件和造楼房一样需要设计,但是和建筑行业严谨客观的设计规范不同,软件设计常常很主观,且容易引发争论。
2936 4
谈谈过度设计:因噎废食的陷阱
|
Rust 前端开发 JavaScript
2022年语言和框架我们值得关注什么?
前端新技术一如既往的更新迭代快:前几天 Next.js 大会,邀请了 vercel 的 CEO 来讲 vercel 的认知。顺带推出了 turbopack,号称比 webpack 快 700 倍,比vite 快 10 倍。又有很多前端同学在问,我们要学 Rust 了吗?新的内容更新迭代太快了,我们跟不上?今年的终端 D2 会有哪些内容?会不会讲 Rust ?
822 119
2022年语言和框架我们值得关注什么?
|
存储 缓存 监控
如何写出一篇好的技术方案?
近期作者在写某个项目的技术方案时,来来回回修改了许多版,很是苦恼。于是,将自己之前写的和别人写的技术方案都翻出来看了几遍,产生了一些思考,分享给大家。
如何写出一篇好的技术方案?
|
缓存 前端开发 Java
是什么让一段20行代码的性能提升了10倍
性能优化显而易见的好处是能够节约机器资源。如果一个有2000台服务器的应用,整体性能提升了10%,理论上来说,就相当于节省了200台的机器。除了节省机器资源外,性能好的应用相对于性能差的应用,在应对流量突增时更不容易达到机器的性能瓶颈,在同样流量场景下进行机器扩容时,也只需要更少的机器,从而能够更快的完成扩容、应急操作。所以,性能好的应用相对于性能差的应用在稳定性方面也更胜一筹。
是什么让一段20行代码的性能提升了10倍
|
Web App开发 机器学习/深度学习 人工智能
使用魔搭开发自己的语音AI:从入门到精通
语音AI是最早从实验室走向应用的AI技术,其发展史就是不断创新、解锁应用的历史,从1995年 Dragon Dictate的桌面孤立词语音识别,到2011年苹果的手机语音助手SIRI,再到当下百花齐放的各种智能语音应用。
1327 41
使用魔搭开发自己的语音AI:从入门到精通
|
存储 运维 监控
亿级异构任务调度框架设计与实践
阿里云日志服务作为云原生可观测与分析平台。提供了一站式的数据采集、加工、查询分析、可视化、告警、消费与投递等功能。全面提升用户的研发、运维、运营、安全场景的数字化能力。日志服务平台作为可观测性平台提供了数据导入、数据加工、聚集加工、告警、智能巡检、导出等功能,这些功能在日志服务被称为任务,并且具有大规模的应用,接下来主要介绍下这些任务的调度框架的设计与实践。
亿级异构任务调度框架设计与实践
|
移动开发 前端开发 JavaScript
跨端开发浪潮中的变与不变
大家好,我是莫觉。今年我将担任阿里巴巴 D2 终端技术大会「跨端技术」的出品人,借由此次机会,写下本文聊聊跨端技术的现状与未来,希望可以给大家带来一些新的启迪。 D2 官网留在了文章最后,大家可以自行查看了解详情。
606 20
跨端开发浪潮中的变与不变