我们项目的区块链存证模块最开始写得很烂。代码混乱。难以维护。bug不断。后来我花了两周时间重构。把烂代码改成了优雅代码。本文记录了这次重构的过程。包括原来的代码有什么问题。重构的思路和步骤。用了哪些设计模式和原则。重构之后的效果。以及一些代码重构的经验总结。希望能给正在做代码重构的你一些参考。
一、背景
先说说这个项目的背景。
我们公司做了一个区块链存证系统。用户可以把文件的哈希存到区块链上。作为存证。以后需要的时候可以验证文件有没有被篡改。
这个模块最开始是一个同事赶项目的时候写的。时间紧任务重。怎么快怎么来。结果代码写得非常烂。
后来那个同事离职了。这个模块就交给我维护了。我接手之后发现。代码简直是一团糟。每次改需求都要花很长时间理解代码。而且改了这里那里又出bug。非常痛苦。
终于有一次。因为一个bug导致存证失败。影响了线上用户。领导很生气。让我彻底解决这个问题。我就下定决心。花两周时间把这个模块重构了。
二、原来的代码有什么问题
先说说原来的代码有什么问题。
问题1:所有逻辑都在一个文件里
原来的代码。所有逻辑都在一个叫EvidenceService.java的文件里。这个文件有两千多行。从参数校验。到业务逻辑。到底层调用。到异常处理。全在这一个文件里。
每次找一个功能都要翻半天。改一个地方要担心会不会影响其他地方。因为所有逻辑都耦合在一起。
问题2:复制粘贴严重
代码里有大量的复制粘贴。比如存证的逻辑。普通文件存证和大文件存证。逻辑差不多。就复制了一份改了改。结果有bug的时候。要改好几个地方。经常漏改。
还有参数校验的代码。每个方法开头都复制了一段。改校验规则要改好几个地方。
问题3:硬编码满天飞
代码里有大量的硬编码。比如区块链节点的地址。Gas价格。合约地址。超时时间。全都是写死在代码里的字符串和数字。
换个环境就要改代码。而且这些值散落在代码的各个地方。改的时候很容易漏。
问题4:异常处理混乱
异常处理非常混乱。有的地方catch了异常直接吞掉。什么都不做。有的地方catch了打印一下堆栈就继续。有的地方抛了RuntimeException。没有统一的异常体系。
出了问题根本不知道是哪里错了。因为异常都被吞掉了。或者被包装成了莫名其妙的RuntimeException。
问题5:没有单元测试
整个模块一个单元测试都没有。改代码全靠手动测试。每次改完都要手动点一遍。而且手动测试覆盖不了所有场景。经常改出bug。
问题6:命名不规范
变量和方法的命名很随意。有的用拼音。有的用缩写。有的名字和实际功能不符。比如有个方法叫saveEvidence。实际上做的是验证存证。看名字根本不知道是干嘛的。
问题7:区块链调用和业务逻辑混在一起
区块链的底层调用。比如创建交易。签名。发送。等待确认。和业务逻辑混在一起。代码里到处都是web3j的API调用。想换一个区块链框架都换不了。因为耦合太深了。
三、重构的目标和原则
分析了这些问题之后。我制定了重构的目标和原则。
目标:
- 代码结构清晰。职责分离。
- 消除重复代码。
- 消除硬编码。用配置管理。
- 统一异常处理。
- 补充单元测试。
- 规范命名。
- 区块链调用和业务逻辑解耦。
原则:
- 不改变外部行为。重构只改内部实现。对外的接口不变。
- 小步快跑。每次改一点。改完就测试。不要一下子改太多。
- 随时可以回滚。用Git管理。每次重构提交一次。出了问题可以回滚。
- 先写测试再重构。对于要改的代码。先补测试。确保重构之后行为不变。
四、重构的步骤
按照上面的目标和原则。我开始了重构。
第一步:梳理现有逻辑。补集成测试
重构之前。我先把现有的逻辑梳理了一遍。画了流程图。搞清楚每个方法是做什么的。输入输出是什么。
然后我写了一些集成测试。覆盖主要的业务场景。比如正常存证。存证失败。验证成功。验证失败等。这些测试在重构之前就能跑通。重构之后也要能跑通。这样就能保证重构不改变外部行为。
第二步:拆分大文件。按职责分层
第一步是把那个两千多行的大文件拆开。按照职责分层。
我把代码分成了几层:
- Controller层:接收请求。参数校验。返回响应。
- Service层:业务逻辑。编排各个步骤。
- Repository层:数据访问。和数据库交互。
- BlockchainClient层:区块链调用封装。所有和区块链相关的操作都在这里。
- DTO/VO:数据传输对象。
拆分之后。每个类的职责都很清晰。代码量也大大减少。最长的类也只有几百行。
第三步:消除重复代码。提取公共方法
拆分之后。我开始消除重复代码。
比如原来有好几处存证的逻辑。只是参数略有不同。我把公共的部分提取成了一个方法。不同的部分用参数或者策略模式来处理。
参数校验的代码也提取成了统一的校验方法。用注解或者工具类来做。不用每个方法都复制一遍。
第四步:消除硬编码。用配置管理
把所有硬编码的值都提取到配置文件里。比如区块链节点地址。Gas价格。合约地址。超时时间等。
用Spring的@Value或者@ConfigurationProperties来注入。这样换环境只需要改配置文件。不需要改代码。
而且配置有默认值。有注释。说明每个配置是做什么的。怎么填。
第五步:统一异常处理
建立了统一的异常体系。定义了业务异常类BusinessException。所有业务相关的异常都用这个类。并且定义了错误码和错误信息的枚举。
底层的异常统一捕获。包装成业务异常抛出。在Controller层统一处理。返回统一格式的错误响应。
这样出了问题。一看错误码就知道是哪里错了。而且异常信息很清晰。不会再出现异常被吞掉的情况。
第六步:规范命名
把所有不规范的命名都改了。变量和方法用有意义的英文单词。见名知意。
比如原来的saveEvidence方法实际上是验证存证。就改成了verifyEvidence。原来的拼音变量都改成了英文。原来的缩写都展开成完整的单词。
改完之后。代码的可读性大大提高。看名字就知道是做什么的。不需要猜。
第七步:区块链调用封装
这是最重要的一步。把所有区块链相关的调用都封装到BlockchainClient类里。
定义了一个接口。比如:
- submitEvidence(String hash):提交存证。返回交易哈希。
- getEvidence(String hash):查询存证。返回存证信息。
- verifyEvidence(String hash, String transactionHash):验证存证。
然后用web3j实现这个接口。业务层只依赖接口。不依赖具体的实现。
这样以后如果要换区块链框架。或者换一条链。只需要写一个新的实现类。业务代码完全不用改。
而且封装之后。业务代码里再也看不到web3j的API了。都是清晰的业务方法。可读性大大提高。
第八步:补充单元测试
重构的过程中。我给每个类都补了单元测试。特别是Service层和BlockchainClient层。
用Mockito模拟依赖。测试每个方法的各种情况。正常情况。异常情况。边界情况。
补了单元测试之后。改代码就有信心了。跑一遍测试就知道有没有改坏。
五、用了哪些设计模式和原则
重构过程中。用了一些设计模式和原则。这里简单说说。
1. 单一职责原则
每个类只做一件事。Controller只做请求处理。Service只做业务逻辑。Repository只做数据访问。BlockchainClient只做区块链调用。职责清晰。
2. 依赖倒置原则
业务层依赖抽象的接口。不依赖具体的实现。比如BlockchainClient是接口。具体的web3j实现可以替换。
3. 策略模式
对于不同类型的存证。比如普通文件存证。大文件存证。哈希存证。用策略模式来处理。每个策略实现一个接口。根据类型选择不同的策略。
这样新增一种存证类型。只需要加一个策略类。不需要改原来的代码。符合开闭原则。
4. 模板方法模式
对于存证的流程。用模板方法模式。定义一个抽象的存证模板。包含参数校验。准备数据。提交区块链。保存记录。发送通知等步骤。
不同类型的存证可以重写某些步骤。但是整体流程是统一的。避免了重复代码。
5. 建造者模式
对于复杂的参数对象。用建造者模式来构建。比构造函数参数一堆要清晰得多。
6. 门面模式
对于复杂的区块链调用。用门面模式封装。提供一个简单的接口。内部封装了创建交易。签名。发送。等待确认等复杂步骤。
调用方只需要调用一个方法。不需要知道内部的复杂流程。
六、重构之后的效果
重构完成之后。效果非常明显。
1. 代码量减少了
虽然拆分了很多类。但是因为消除了重复代码。总代码量从原来的两千多行减少到了一千多行。而且结构更清晰。
2. 可读性大大提高
命名规范了。职责清晰了。注释也补了。现在新同事接手。看一遍代码就能理解。不需要像我之前那样花好几天去猜。
3. bug减少了
因为有了单元测试和集成测试。改代码之后跑一遍测试就知道有没有问题。上线之后的bug大大减少。
4. 维护成本降低了
以前改一个需求要花好几天。现在因为结构清晰。改起来很快。而且不容易改出bug。维护成本大大降低。
5. 可扩展性提高了
因为用了策略模式和依赖倒置。新增功能很方便。比如新增一种存证类型。只需要加一个策略类。不需要改原来的代码。
6. 可以换区块链了
因为区块链调用封装成了接口。后来我们真的换了一条链。只写了一个新的BlockchainClient实现。业务代码一行都没改。一周就完成了切换。如果是原来的代码。估计要改一个月。
七、代码重构的经验总结
这次重构给了我很多经验。总结一下。
1. 重构之前一定要有测试
这是最重要的。没有测试的重构就是赌博。你不知道改完之后行为是不是对的。
所以重构之前。先补测试。哪怕是集成测试也好。有了测试。重构才有底气。
2. 小步快跑。不要贪多
不要想着一次把所有问题都改完。每次改一点。改完就测试。确认没问题了再改下一点。
如果一次改太多。出了问题都不知道是哪里改坏的。回滚也麻烦。
3. 不要在重构的同时加新功能
重构的时候只改代码结构。不要加新功能。因为加新功能会改变行为。你就分不清是重构出了问题还是新功能出了问题。
如果要加新功能。等重构完成之后再加。
4. 领导的支持很重要
重构是有风险的。而且短期内看不到产出。需要领导的理解和支持。
我这次重构之前。和领导充分沟通了。说明了原来的代码有什么问题。重构的好处是什么。需要多长时间。领导同意了我才开始的。
如果领导不支持。你偷偷摸摸地重构。出了问题就麻烦了。
5. 不要追求完美
重构不可能一次做到完美。先解决最严重的问题。比如大文件拆分。重复代码消除。剩下的可以慢慢优化。
追求完美会导致重构永远做不完。而且可能过度设计。
6. 重构是持续的
重构不是一次就完了。代码会不断腐化。需要持续重构。每次加新功能的时候。顺便重构一下相关的代码。保持代码的健康。
八、给想做重构的人的建议
如果你也想做代码重构。我有几个建议。
1. 先搞清楚为什么要重构
不要为了重构而重构。要明确重构的目标。是为了提高可读性。还是为了提高性能。还是为了增加可扩展性。目标明确了。重构才有方向。
2. 评估风险和收益
重构是有成本和风险的。要评估一下。值不值得重构。如果代码马上就要废弃了。那就没必要重构了。如果代码要长期维护。而且问题很严重。那就值得重构。
3. 做好计划
重构之前做好计划。分几个步骤。每个步骤做什么。需要多长时间。怎么验证。按计划来。不要想到哪改到哪。
4. 和团队沟通
如果是团队项目。重构之前要和团队沟通。让大家知道你在重构。避免两个人同时改同一份代码。产生冲突。
5. 保留旧代码一段时间
重构完成之后。旧代码不要马上删掉。保留一段时间。确认新代码没有问题了再删。万一新代码有问题。还可以回滚到旧代码。
九、写在最后
这次区块链存证模块的重构。是我做过的最有成就感的事情之一。看着一团糟的代码。在自己的手里变得清晰优雅。那种感觉非常好。
而且重构之后的代码。确实给团队带来了实实在在的好处。bug少了。维护容易了。加新功能快了。每个人都受益。
我想说的是。烂代码不可怕。可怕的是烂代码一直没人管。越积越多。最后变成没人敢碰的屎山。只要我们有勇气去重构。有方法地去重构。烂代码也能变成优雅代码。
当然重构不是目的。写出好代码才是目的。希望我们每个人都能写出优雅的代码。也有勇气去重构烂代码。
最后用一句话结束本文:"代码是写给人看的。只是顺便能在机器上运行。"愿每一个开发者都能写出优雅的代码。也能勇敢地面对烂代码。把它变得优雅。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录