让代码优雅起来:记一次代码微重构实践

代码,优雅,起来,一次,重构,实践 · 浏览次数 : 126

小编点评

京东零售 加文雄来源:京东云开发者社区。归纳总结以上内容,生成内容时需要带简单的排版 **重构:改善既有代码的设计** 作者:京东零售 加文雄来源:京东云开发者社区。归纳总结以上内容,生成内容时需要带简单的排版 **重构的每个步骤都很简单,甚至显得有些过于简单:** * 你只需要把某个字段从一个类移到另一个类 * 你只需要把某些代码从一个函数拉出来构成另一个函数 * 在继承体系中把某些代码推上推下 **聚沙成塔,这些小小的修改累积起来就可以根本改善设计质量。** **重构不仅能够提高代码质量,让代码优雅起来,同时也能让我们学以致用。**

正文

一、需求开发修改代码

一次需求开发时碰到如下所示方法代码:

private OrderShoudSettlementAmount getOrderShoudSettlementAmount(OrderDTO orderMain, List<SettlementDetail> details) {
    OrderShoudSettlementAmount settlementAmount = new OrderShoudSettlementAmount();
    // 应结金额=33021-33002-32003+32001-31001
    // 货款佣金=33005+33002+32003+31001
    long feeMoney33021 = 0;
    long feeMoney33002 = 0;
    long feeMoney32003 = 0;
    long feeMoney32001 = 0;
    long feeMoney31001 = 0;
    long feeMoney33005 = 0;
    for (SettlementDetail settlementDetail : details) {
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_ZS_NOSETTLE.getVal())) {
            feeMoney33021 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE.getVal())) {
            feeMoney33002 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ.getVal())) {
            feeMoney32003 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_NOSETTLE.getVal())) {
            feeMoney32001 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE.getVal())) {
            feeMoney31001 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_YFYJ_ZS_XSG.getVal())) {
            feeMoney33005 += settlementDetail.getOassMoney();
        }
    }
    long settlementMoney = feeMoney33021 - feeMoney33002 - feeMoney32003 + feeMoney32001 - feeMoney31001;
    long goodCommissionMoney = feeMoney33005 + feeMoney33002 + feeMoney32003 + feeMoney31001;
    settlementAmount.setSettlementAmount(settlementMoney);
    settlementAmount.setGoodsCommission(goodCommissionMoney);
    settlementAmount.setOrderId(orderMain.getOrderId());
    settlementAmount.setOrgCode(orderMain.getOrgCode());
    settlementAmount.setStationNo(String.valueOf(orderMain.getDeliveryStationNo()));
    settlementAmount.setBillTime(new Date());
    settlementAmount.setRetSuccess(false);
    return settlementAmount;
}

该方法逻辑比较简单,就是组装OrderShoudSettlementAmount对象。其中需要计算2个金额,分别是settlementMoney和goodCommissionMoney。

本次需求新增了费项,需要修改该方法。代码修改后如下所示:

private OrderShoudSettlementAmount getOrderShoudSettlementAmount(OrderDTO orderMain, List<SettlementDetail> details) {
    OrderShoudSettlementAmount settlementAmount = new OrderShoudSettlementAmount();
    // 应结金额=33021-33002-32003+32001-31001+34012-34013
    // 货款佣金=33005+33002+32003+31001+34013
    long feeMoney33021 = 0;
    long feeMoney33002 = 0;
    long feeMoney32003 = 0;
    long feeMoney32001 = 0;
    long feeMoney31001 = 0;
    long feeMoney33005 = 0;
    // 本次需求新增费项
    long feeMoney34012 = 0;
    // 本次需求新增费项
    long feeMoney34013 = 0;
    for (SettlementDetail settlementDetail : details) {
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_ZS_NOSETTLE.getVal())) {
            feeMoney33021 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE.getVal())) {
            feeMoney33002 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ.getVal())) {
            feeMoney32003 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_NOSETTLE.getVal())) {
            feeMoney32001 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE.getVal())) {
            feeMoney31001 += settlementDetail.getOassMoney();
        }
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_YFYJ_ZS_XSG.getVal())) {
            feeMoney33005 += settlementDetail.getOassMoney();
        }
        // 本次需求新增费项
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_CHF_XSG_NOSETTLE.getVal())) {
            feeMoney34012 += settlementDetail.getOassMoney();
        }
        // 本次需求新增费项
        if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_CHF_YJ_XSG.getVal())) {
            feeMoney34013 += settlementDetail.getOassMoney();
        }
    }
    // 本次需求新增费项追加计算 + feeMoney34012 - feeMoney34013
    long settlementMoney = feeMoney33021 - feeMoney33002 - feeMoney32003 + feeMoney32001 - feeMoney31001 + feeMoney34012 - feeMoney34013;
    // 本次需求新增费项追加计算 + feeMoney34013
    long goodCommissionMoney = feeMoney33005 + feeMoney33002 + feeMoney32003 + feeMoney31001 + feeMoney34013;
    settlementAmount.setSettlementAmount(settlementMoney);
    settlementAmount.setGoodsCommission(goodCommissionMoney);
    settlementAmount.setOrderId(orderMain.getOrderId());
    settlementAmount.setOrgCode(orderMain.getOrgCode());
    settlementAmount.setStationNo(String.valueOf(orderMain.getDeliveryStationNo()));
    settlementAmount.setBillTime(new Date());
    settlementAmount.setRetSuccess(false);
    return settlementAmount;
}

二、嗅出代码的坏味道

Martin Fowler在《重构:改善既有代码的设计》一书中列出了22种代码的坏味道:

1.Duplicated Code(重复的代码) 2.Long Method(过长函数) 3.Large Class(过大类) 4.Long Parameter List(过长参数列) 5.Divergent Change(发散式变化) 6.Shotgun Surgery(霰弹式修改) 7.Feature Envy(依恋情结) 8.Data Clumps(数据泥团) 9.Primitive Obsession(基本型别偏执) 10.Switch Statements(switch惊悚现身) 11.Parallel Inheritance Hierarchies(平行继承体系) 12.Lazy Class(冗赘类) 13.Speculative Generality(夸夸其谈未来性) 14.Temporary Field(令人迷惑的暂时字段) 15.Message Chains(过度耦合的消息链) 16.Middle Man(中间人) 17.Inappropriate Intimacy(狎昵关系) 18.Alternative Classes with Different Interfaces(异曲同工的类) 19.Incomplete Library Class(不完美的程序库类) 20.Data Class(纯稚的数据类) 21.Refused Bequest(被拒绝的遗贈) 22.Comments(过多的注释)

参照这22种代码的坏味道,我在以上方法代码中嗅出了2种代码的坏味道:

坏味道1:Duplicated Code(重复的代码)

for循环中对每种费项的累加操作是重复代码,而且每次新增费项,还得不断增加该重复操作。

for (SettlementDetail settlementDetail : details) {
    if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_ZS_NOSETTLE.getVal())) {
        feeMoney33021 += settlementDetail.getOassMoney();
    }
    if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE.getVal())) {
        feeMoney33002 += settlementDetail.getOassMoney();
    }
    if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ.getVal())) {
        feeMoney32003 += settlementDetail.getOassMoney();
    }
    if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_NOSETTLE.getVal())) {
        feeMoney32001 += settlementDetail.getOassMoney();
    }
    if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE.getVal())) {
        feeMoney31001 += settlementDetail.getOassMoney();
    }
    if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_YFYJ_ZS_XSG.getVal())) {
        feeMoney33005 += settlementDetail.getOassMoney();
    }
    // 本次需求新增费项
    if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_CHF_XSG_NOSETTLE.getVal())) {
        feeMoney34012 += settlementDetail.getOassMoney();
    }
    // 本次需求新增费项
    if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_CHF_YJ_XSG.getVal())) {
        feeMoney34013 += settlementDetail.getOassMoney();
    }
}

坏味道2:Divergent Change(发散式变化)

Martin Fowler在书中对该坏味道的部分解释如下:

我们希望软件能够更容易被修改——毕竟软件再怎么说本来就该是“软”的。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。

现在该方法代码因为新需求开发,修改多处。

其实,除了以上2种代码的坏味道之外,该方法代码最大的问题是面向过程式编码而不是面向对象式的。

为什么这么说呢?

前面提到过该方法的主要作用是组装OrderShoudSettlementAmount对象,那么其逻辑就应该主要体现“组装”,而不是计算金额。计算金额相关逻辑应该抽离到单独的类中,这样既符合面向对象编程思想,也能够消除坏味道2

三、重构代码

针对前面嗅出的代码坏味道,果断进行重构。重构之后代码如下所示:

private OrderShoudSettlementAmount getOrderShoudSettlementAmount(OrderDTO orderMain, List<SettlementDetail> details) {
    OrderShoudSettlementAmount settlementAmount = new OrderShoudSettlementAmount();
    
    Map<Integer, Long> expenseTypeToFeeMoneyMap = Maps.newHashMap();
    for (SettlementDetail settlementDetail : details) {
        long feeMoney = Optional.ofNullable(expenseTypeToFeeMoneyMap.get(settlementDetail.getExpenseType())).orElse(0L);
        feeMoney += Optional.ofNullable(settlementDetail.getOassMoney()).orElse(0L);
        expenseTypeToFeeMoneyMap.put(settlementDetail.getExpenseType(), feeMoney);
    }
    long settlementMoney = SettlementMoneyCalcFeeInfoEnum.calcSettlementMoney(expenseTypeToFeeMoneyMap);
    long goodCommissionMoney = GoodCommissionMoneyCalcFeeInfoEnum.calcGoodCommissionMoney(expenseTypeToFeeMoneyMap);
    settlementAmount.setSettlementAmount(settlementMoney);
    settlementAmount.setGoodsCommission(goodCommissionMoney);
    settlementAmount.setOrderId(orderMain.getOrderId());
    settlementAmount.setOrgCode(orderMain.getOrgCode());
    settlementAmount.setStationNo(String.valueOf(orderMain.getDeliveryStationNo()));
    settlementAmount.setBillTime(new Date());
    settlementAmount.setRetSuccess(false);
    return settlementAmount;
}
enum SettlementMoneyCalcFeeInfoEnum {
    /**计算项*/
    FEE_33021(FeeInfoEnum.FEE_INFO_FREIGHT_ZS_NOSETTLE, "+"),
    FEE_33002(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE, "-"),
    FEE_32003(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ, "-"),
    FEE_32001(FeeInfoEnum.FEE_INFO_GOODS_NOSETTLE, "+"),
    FEE_31001(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE, "-"),
    FEE_34012(FeeInfoEnum.FEE_INFO_CHF_XSG_NOSETTLE, "+"),
    FEE_34013(FeeInfoEnum.FEE_INFO_CHF_YJ_XSG, "-");

    private final FeeInfoEnum feeInfoEnum;
    private final String symbol;

    SettlementMoneyCalcFeeInfoEnum(FeeInfoEnum feeInfoEnum, String symbol) {
        this.feeInfoEnum = feeInfoEnum;
        this.symbol = symbol;
    }
    
    public static long calcSettlementMoney(Map<Integer, Long> expenseTypeToFeeMoneyMap) {
        // 应结金额=33021-33002-32003+32001-31001+34012-34013
        long settlementMoney = 0L;
        for (SettlementMoneyCalcFeeInfoEnum calcFeeInfoEnum : SettlementMoneyCalcFeeInfoEnum.values()) {
            if ("+".equals(calcFeeInfoEnum.symbol)) {
                settlementMoney += Optional
                        .ofNullable(expenseTypeToFeeMoneyMap.get(calcFeeInfoEnum.feeInfoEnum.getVal()))
                        .orElse(0L);
            }
            if ("-".equals(calcFeeInfoEnum.symbol)) {
                settlementMoney -= Optional
                        .ofNullable(expenseTypeToFeeMoneyMap.get(calcFeeInfoEnum.feeInfoEnum.getVal()))
                        .orElse(0L);
            }
        }
        return settlementMoney;
    }
}
enum GoodCommissionMoneyCalcFeeInfoEnum {
    /**计算项*/
    FEE_33005(FeeInfoEnum.FEE_INFO_YFYJ_ZS_XSG),
    FEE_33002(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE),
    FEE_32003(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ),
    FEE_31001(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE),
    FEE_34013(FeeInfoEnum.FEE_INFO_CHF_YJ_XSG);

    private final FeeInfoEnum feeInfoEnum;

    GoodCommissionMoneyCalcFeeInfoEnum(FeeInfoEnum feeInfoEnum) {
        this.feeInfoEnum = feeInfoEnum;
    }

    public static long calcGoodCommissionMoney(Map<Integer, Long> expenseTypeToFeeMoneyMap) {
        // 货款佣金=33005+33002+32003+31001+34013
        long goodCommissionMoney = 0L;
        for (GoodCommissionMoneyCalcFeeInfoEnum calcFeeInfoEnum : GoodCommissionMoneyCalcFeeInfoEnum.values()) {
            goodCommissionMoney += Optional
                    .ofNullable(expenseTypeToFeeMoneyMap.get(calcFeeInfoEnum.feeInfoEnum.getVal()))
                    .orElse(0L);
        }
        return goodCommissionMoney;
    }
}

四、总结

以上重构的方法代码比较简单,有些人可能会觉得不重构也挺好的,代码可读性也不差,每次修改也就肉眼可见的几个地方,没必要在这上面花费时间。

如果你有以上想法,不妨了解下软件工程中的“破窗效应”:

破窗效应指的是在软件开发过程中,如果存在低质量的代码或设计,如果不及时修复,就会导致其他开发人员也采用同样的低质量方案。这会逐渐升级到更严重的问题,导致软件系统变得难以维护、扩展和改进。因此,在软件开发中,及时解决问题和保持代码质量非常重要,以避免破窗效应对于整个项目造成的负面影响。

同时看看Martin Fowler在《重构:改善既有代码的设计》一书中对重构的部分解释:

重构的每个步骤都很简单,甚至显得有些过于简单:你只需要把某个字段从一个类移到另一个类,把某些代码从一个函数拉出来构成另一个函数,或是在继承体系中把某些代码推上推下就行了。但是,聚沙成塔,这些小小的修改累积起来就可以根本改善设计质量。

重构不仅能够提高代码质量,让代码优雅起来,同时也能让我们学以致用。我们所学的设计思想、原则、模式等理论知识,往往在重构中能够真正实践。

作者:京东零售 加文雄

来源:京东云开发者社区

与让代码优雅起来:记一次代码微重构实践相似的内容:

让代码优雅起来:记一次代码微重构实践

重构不仅能够提高代码质量,让代码优雅起来,同时也能让我们学以致用。我们所学的设计思想、原则、模式等理论知识,往往在重构中能够真正实践。

spannerlib优雅的go异常处理

蹩脚的go 异常处理 一般写go的人,如果他不是写算法,正常写业务代码的话,可能都会为优雅的异常处理而烦恼,因为脑子抽筋的go设计者们,总是感觉语法糖是一种很低级的东西。但是在我们大多数公司的业务逻辑中,没有语法糖让代码非常丑陋,不易于维护。 如何让go 代码更具有可读性,哪么就要给go加糖! 引入

这样也行,在lambda表达式中优雅的处理checked exception

简介 最近发现很多小伙伴还不知道如何在lambda表达式中优雅的处理checked exception,所以今天就重点和大家来探讨一下这个问题。 lambda表达式本身是为了方便程序员书写方便的工具,使用lambda表达式可以让我们的代码更加简洁。 可能大多数小伙伴在使用的过程中从来没有遇到过里面包

时间老去,Ruby不死,Ruby语言基础入门教程之Ruby3全平台开发环境搭建EP00

如果说电子游戏是第九艺术,那么,编程技术则配得上第十艺术的雅称。艺术发展的普遍规律就是要给与人们对于艺术作品的更高层感受,而Matz的Ruby语言则正是这样一件艺术品。 无论是语法还是理念,都让Ruby开发者感受到款待,如此,Ruby代码就像活了过来,它们时而高声,却藏不住优雅,时而细语,却意外地铿

复杂度分析:如何分析、统计算法的执行效率和资源消耗

我们都知道,数据结构和算法本身解决的是“快”和“省”的问题,即如何让代码运行得更快,如何让代码更省存储空间。所以,执行效率是算法一个非常重要的考量指标。那如何来衡量你编写的算法代码的执行效率呢?这里就要用到我们今天要讲的内容:时间、空间复杂度分析。

『手撕Vue-CLI』编码规范检查

前言 这篇为什么是编码规范检查呢?因为这是一个很重要的环节,一个好的编码规范可以让代码更加清晰易读,在官方的 VUE-CLI 也是有着很好的编码规范的,所以我也要加入这个环节。 其实不管在哪个项目中,编码规范都是很重要的,像我们平日里的项目开发当中,我们也会使用 ESLint 来进行代码规范检查。

Dlang 并行化

# Dlang 并行化 > 好难受,dlang 生态太差,没办法,学了半天才明白。 > > 我尽量以精炼的语言解释。 > > 采用 定义,例子(代码),解释 的步骤讲解。 > > 所以你可能看到很多代码,一点解释…… > > 我会省略一些 `import`,让代码短一些 [TOC] ## `para

C#的基于.net framework的Dll模块编程(三) - 编程手把手系列文章

继续这个系列的博文: 一、设置DLL类库信息; 在接解决方案资源管理器中选择该Dll程序集项目,鼠标右键,选择属性,打开窗口。 点击“程序集信息”,打开并编辑该Dll程序集的相关信息; 二、代码折叠注释操作; 为了在编辑代码的时候让代码更加美观和专注性,需要将部分代码进行折叠,既做了注释,又能够将该

零代码,让业务人员实现应用创造自由

摘要:以汽车营销场景为例,从AppCube零代码和业务大屏入手,帮助开发者更好地理解AppCube低代码和零代码异同点,在实际使用时能更快选取更合适的工具能力,实现应用构建效率最大化。 本文分享自华为云社区《DTT第8期直播回顾 | 零代码,让业务人员实现应用创造自由》,作者:华为云社区精选 。 本

精选版:用Java扩展Nginx(nginx-clojure 入门)

让 Java 代码直接在 Nginx 上运行?这么有趣的功能,随本文一起来实战体验吧,图文并茂,一定能成功的那种实战