让代码优雅起来:记一次代码微重构实践 | 京东云技术团队

京东云开发者
• 阅读 372

一、需求开发修改代码

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

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在《重构:改善既有代码的设计》一书中对重构的部分解释:

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

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

作者:京东零售 加文雄

来源:京东云开发者社区

点赞
收藏
评论区
推荐文章
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
九路 九路
4年前
Java实现 一篇文章说尽设计模式之六大原则
我们知道,设计模式很有用,学好设计模式不但能让你写出更简洁,优雅的代码,还能使得代码的结构更清晰,也更有利于扩展当然设计模式也不是万能的,一成不变的.设计模式只是前人总结出来的一种经验,一种特定问题的解决方法,不能看作是死的东西不一定非要生搬硬套,非得按照设计模式书上来来,只要我们写的代码符合一定的一些原则,一样可以看作是自己的模式.但是前人总结
万木春 万木春
1年前
高效易用的C++单元测试框架:轻松构建高质量代码
单元测试是构建稳定、高质量的程序、服务或系统的必不可少的一环。通过单元测试,我们可以在开发过程中及时发现和修复代码中的问题,提高代码的质量和可维护性。同时,单元测试也可以帮助我们更好地理解代码的功能和实现细节,从而更好地进行代码重构和优化。
京东云开发者 京东云开发者
2个月前
「重构:改善既有代码的设计」实战篇
背景在软件开发的世界里,代码重构是提升项目质量、适应业务变化的关键步骤。最近,我重新翻阅了《重构:改善既有代码的设计第二版》,这本书不仅重新点燃了我对重构的热情,还深化了我的理解:重构不仅仅是代码层面的整理,它更是一种软件开发的哲学,强调持续改进和适应变化
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
Java中级进阶篇
中级进阶篇1.《重构:改善既有代码的设计》适合对象:中级、高级介绍:清晰揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以及改善。大师的杰作,让你明白软件是一步步改进的,不是一蹴而就的。2.《XML入门经典》适合对象:初级、中级、高级介绍:很多人觉得没必要买XML书籍,
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
刚背兽 刚背兽
3个月前
跨平台集成开发环境JetBrains CLion 2024 for Mac中文激活版
JetBrAInsCLion2024forMac是一款专为Mac用户设计的强大C集成开发环境(IDE)。它具备出色的代码自动补全功能,能够根据上下文智能推荐代码选项,提高编程效率。同时,CLion2024还提供了代码重构、导航和集成调试等核心功能,帮助