让代码优雅起来:记一次代码微重构实践 | 京东云技术团队
一、需求开发修改代码
一次需求开发时碰到如下所示方法代码:
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在《重构:改善既有代码的设计》一书中对重构的部分解释:
重构的每个步骤都很简单,甚至显得有些过于简单:你只需要把某个字段从一个类移到另一个类,把某些代码从一个函数拉出来构成另一个函数,或是在继承体系中把某些代码推上推下就行了。但是,聚沙成塔,这些小小的修改累积起来就可以根本改善设计质量。
重构不仅能够提高代码质量,让代码优雅起来,同时也能让我们学以致用。我们所学的设计思想、原则、模式等理论知识,往往在重构中能够真正实践。
作者:京东零售 加文雄
来源:京东云开发者社区

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
东方通信基于 KubeSphere 的云计算落地经验
作者:周峰 吴昌泰 公司简介 东方通信股份有限公司(以下简称“东方通信”)创立于 1958 年,是一家集硬件设备、软件、服务为一体的整体解决方案提供商。公司于 1996 年成功改制上市,成为上海证交所同时发行 A 股和 B 股的国有控股上市公司。公司业务主要包括:专网通信及信息安全产品和解决方案、公网通信相关产品及 ICT 服务、金融电子设备及软件产品、智能制造业务。 十四五期间,公司主责主业聚焦在以专网通信、公网通信、ICT 服务为基础的“信息通信产业”,金融电子为基础的“金融科技产业”和“智能制造产业”三大产业,围绕主责主业不断创新与转型升级。 肩负“科技创造价值,共筑美好生活”的使命,东方通信坚持“诚信、务实、创新、共赢”的理念,致力于为客户提供优质的产品、便利的体验、完美的方案和满意的服务,努力成为在国际市场中拥有优势品牌、持续创新和发展的领先企业! 技术现状 在使用 KubeSphere 之前,公司已经开始研究云原生和探索应用上云的道路。但是由于每个团队接触和学习云原生技术的时间不同,原生 Kubernetes 的使用还是存在一定的门槛。并且随着公司云的基础设施和使用团队的的...
- 下一篇
发布策略选型:ZadigX、阿里云、Argo、Spinnaker、Harness、Codefresh...
在软件开发和运维的领域中,灰度发布是一种关键的部署策略,用于逐步推送新版本给用户,以减少潜在的风险和影响范围。不同的平台在实现灰度发布时可能存在差异,因为它们需要满足各自的需求和限制。本文将对灰度发布的不同平台进行全面比对,重点关注 ZadigX、阿里云、Harness、Spinnaker、Argo Rollouts 等主流平台。我们将深入探讨它们的使用条件、实现原理、使用流程,横向差异的比对,旨在帮助大家选择最适合自己的平台。 实现原理和使用流程 01、ZadigX ZadigX 支持蓝绿、金丝雀、分批次灰度、Istio 发布等发布策略,下面简单介绍 ZadigX 蓝绿发布原理,更多发布策略使用过程参考官方文档[1]。 使用条件 workload 需要有一个 service 与之对应,并且 workload 的 labels 包含所有 service 的 selector labels workload 当前只支持 deployment 类型 原理 部署蓝环境,复制当前 workload,设置新的镜像,创建一个 blue service 指向它 蓝环境部署完成,执行用户的验证任务 开...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境