代码重构指南
本文介绍了代码重构的类型、方法和最佳实践,帮助读者建立正确的重构概念,从而顺利实现重构,构建可持续的、有生命的代码。原文:The Guide to Code Refactoring

简介
重构是在不改变代码外部行为的情况下改进代码内部架构的过程,旨在增强代码可读性、降低复杂性并提高可维护性。本指南全面介绍了重构的艺术,涵盖了有效管理代码变更的基本概念、类型、目的、挑战和特定技巧。
重构的基本概念
重构是修改代码以改进其内部架构,而不改变其外部行为。
重构的类型
1. 小型重构 —— 仅限于单个类
变量重命名:使变量名称更具描述性,例如,将
i
更改为interstRate
。提取变量:将重复表达式放到到单个变量中。
提取函数:将重复代码块放入单独的函数中,以便更好的重用。
2. 中型重构 —— 跨多个类
提取接口:将常见方法组合成多态接口。
提取父类:将共享的属性和方法移动到公共父类中。
委派:将职责从一个类分配到另一个类,以优化关注点分离。
3. 大规模重构 —— 在系统组件级别。
拆分/合并服务:通过拆分或合并服务来优化系统架构。
组件化:将系统分解为独立的组件,以提高模块化和可维护性。
重构的目的
重构的目的是创建更合乎逻辑的代码架构,增强可读性,降低复杂性,并改善可维护性。
简化代码,使其更容易扩展并适应未来需求,在添加新功能时能最大限度减少复杂性。
增量重构以最小化与大规模变更相关的风险。每次修改后都要进行彻底测试,以确保稳定性。
具体措施
删除重复代码,使代码更简洁和可维护。
为变量和函数使用更具描述性的名称以提高清晰度。
通过提取可重用函数和优化方法参数来分解复杂逻辑。
将代码分解为独立、可重用的模块。
明确定义接口以减少模块耦合。
应用适当的模式来增加灵活性。
随着时间推移,做一些小的、可管理的改变,以积累更大的进步。
实现单元测试、功能测试和回归测试,以验证更改后的行为。
在生产环境中逐步推出更改,以尽量减少对用户的影响。
何时重构
Don Roberts 的“三遍即重构(three strikes and refactor)”原则指出,当一段代码重复出现三次时,就应该对其进行重构,以避免冗余。
将相似代码重构为抽象方法、优化变量并删除冗余逻辑。
在添加新功能时,对现有代码进行小规模重构以保持一致性。
代码审查可从经验丰富的同事那里获得见解,有助于知识传递并提出改进建议。
对于中大型重构,将其纳入需求中。对于大规模变更,制定详细计划并评估影响。
当出现线上问题时,就是进行改进的好时机。
何时不应进行重构
如果由于过于复杂而导致重写比重构更简单,那么重写可能是更好的选择。
稳定性高且很少修改的代码可能无需进行重构。
重构的挑战
困难的重构可能需要时间,从而影响进度。业务支持至关重要。
重构可能需要在旧代码和新代码中同时实现新功能。
不完整的重构会混合旧代码和新代码的逻辑,增加理解代码的成本。
由于复杂性导致的核心业务逻辑或数据库架构可能难以重构。
影响接口的更改可能会破坏其他系统,需要谨慎管理。
长期影响需要进行彻底研究和跟踪。
控制重构风险
使用 IDE 重构工具:使用现代工具确保安全的重构。
自动化测试:进行彻底测试,使重构后的代码的行为与原始代码保持一致。
流量回放测试:在真实环境中验证性能。
灰盒部署:逐步部署更改,以尽量减少对用户的影响。
监控警报:迅速识别并解决问题。
涉及数据库和数据结构的重构
数据库结构变更需要进行精心规划,以确保数据的完整性和一致性。
迁移计划:制定平稳的数据转换方案。
回滚策略:制定回滚策略,以便在出现问题时能够迅速恢复。
用例
在添加新功能的过程中进行重构能够使代码结构更加清晰。
应优先解决 bug,然后再进行重构。
定期审查有助于发现并消除“代码异味”。
重构是敏捷方法中用于持续提高代码质量的核心活动。
常见重构场景
长参数
如果一个方法有过多参数,那么调用该方法就会容易出错,并且会增加复杂度和维护成本。
当方法签名中的参数超过 4 个时,尤其是如果多个参数是布尔类型或枚举类型,这个问题就会变得更加明显。
解决方案:
创建参数对象:将多个参数整合到单一对象中,以减少方法签名中的参数数量。
示例:对于方法
void calculatePay(int basePay, int bonus, boolean isFullTime, boolean isOverTime, String payType)
,将其重构为void calculatePay(PayParams params)
,其中PayParams
对象包含basePay, bonus, isFullTime, isOverTime, payType
属性。使用构造函数或构建器模式
构造器:通过构造函数传递参数以减少参数数量。
构建器模式:使用构建器模式逐步构建参数对象,从而提高代码的可读性和可维护性。
示例:使用构建器模式创建
PayParams
对象,并将其传递给calculatePay
方法。
简化条件逻辑
大量使用条件语句的方法(具有多重嵌套 if-else
语句或过于复杂的条件语句的方法)难以理解和维护。
解决方案:
提取方法:将复杂条件逻辑移至单独的方法中,使主方法更加简洁。
示例:对于包含嵌套
if-else
语句的方法void processOrder(Order order)
,将其重构为void processOrder(Order order) { if (isValidOrder(order)) { ... } }
,其中isValidOrder
包含所有条件逻辑。使用策略模式
策略模式:将不同条件逻辑封装到不同策略类中,并根据条件在主方法中切换。
示例:定义
OrderProcessingStrategy
接口,并实现诸如StandardOrderProcessingStrategy
和SpecialOrderProcessingStrategy
等实现类,然后在processOrder
方法中使用。运用多态性
多态性:通过继承和多态,将不同的条件逻辑分配到不同的子类中。
示例:定义
OrderProcessor
抽象类,并派生出StandardOrderProcessor
和SpecialOrderProcessor
,在主方法中根据条件选择合适的处理类。
分散修改导致高昂的维护成本
当一个功能的实现分散在多个类或方法中时,任何修改都会影响到很多地方,从而增加了维护成本。
实现代码分散在多个文件中,因此每次修改都需要在多个地方进行查找和修改。
解决方案:
提取类
将分散的功能整合到一个单一的类中,使功能更加集中且更易于管理。
示例:假设订单处理的功能分布在
OrderService
、PaymentService
、InventoryService
等多个类中。创建OrderProcessor
类来整合这些功能。使用委托
职责分离:将某些功能委托给其他类,从而减轻主类的职责。
示例:将订单验证委托给
OrderValidator
类,将支付处理委托给PaymentHandler
类。运用设计模式
责任链模式:将多个处理步骤串联起来,每个步骤负责处理功能的一部分。
示例:创建
OrderProcessingChain
类,将订单验证、支付处理和库存检查串联起来,每个步骤由单独的类来管理。
长函数
如果函数代码过长(超过 20 行的函数,尤其是那些包含多个子任务的函数),就会变得难以理解和维护。
解决方案:
提取方法
将子任务拆分为独立的方法,使主函数更加简洁。
示例:对于包含请求解析、数据处理和结果返回的
void processUserRequest(UserRequest request)
方法,将其重构为void processUserRequest(UserRequest request) { parseRequest(request); processData(); returnResult();}}
.使用类
创建类:将多个子任务封装到单独的类中,使代码更具模块化。
示例:创建
UserRequestProcessor
类,以封装parseRequest
、processData
和returnResult
等方法。运用设计模式
模板方法模式:通过使用模板方法模式,将常见处理步骤提取到父类中,让子类负责具体实现。
示例:创建
AbstractRequestProcessor
类,定义常见处理步骤,并创建UserRequestProcessor
子类来负责具体实现。
死代码
死代码指的是那些永远不会被执行的代码(未使用的变量、未被调用的方法、永远不会被执行的分支),通常是因为逻辑错误或遗留问题所致。
解决方案:
静态分析
使用诸如 SonarQube 或 IntelliJ IDEA 之类的静态分析工具来识别并删除无用代码。
示例:删除未使用的变量,例如
int unusedVariable = 0;
或无用的方法,例如void unusedMethod() { ... }
.简化逻辑
简化条件检查:移除那些永远不会执行的分支,例如:
if (false) { ... }}
。示例:删除
if (false) { ... }
这样的分支,因为该分支永远不会被执行。重构旧代码
清理历史代码:在系统维护期间逐步清理过时的代码段。
示例:在系统升级过程中删除诸如
void oldMethod() { ... }
这样的旧版本方法。
大型类
一个类试图承担过多的任务,从而导致代码冗长、复杂且难以理解。
该类包含的代码量过多,导致难以阅读和理解。
该类包含多个不相关的功能,使内部逻辑变得复杂。
该类与其他类高度耦合,这意味着对一个类的更改可能会影响到多个其他类。
解决方案:
提取类
将类分解为更小、更专注的子类,减少每个类的职责范围。
示例:一个包含订单处理、支付处理、库存检查等功能的
OrderProcessor
类,可以拆分为OrderService
、PaymentService
和InventoryService
等子类,每个子类分别负责一项特定功能。运用设计模式
应用诸如责任链模式或策略模式这样的模式来分配类的职责。
示例:使用责任链模式将订单处理的不同步骤(如验证、支付、库存)串联起来,由不同的类分别处理每个步骤。
重复代码
程序中存在过多重复代码,会增加冗余和系统间的耦合性,从而降低可维护性和稳定性。
重复代码会增加代码的长度,并降低可维护性。
重复代码会增加系统间的耦合度,因此在一处进行的更改往往需要在多处进行调整。
重复代码会增加潜在的错误点,并降低系统的稳定性。
解决方案:
提取方法
将重复代码提取到单独的方法中,以提高可复用性。
示例:如果在多个地方存在相同的订单验证逻辑,将其提取到一个名为
isValidOrder(Order order)
的方法中。使用继承或委托
通过使用继承或委托来减少重复代码。
示例:将通用的订单处理逻辑放在基类中,让子类实现特定的逻辑。
使用设计模式
应用诸如模板方法或工厂模式这样的模式来减少重复。
示例:使用模板方法模式将常见处理步骤提取到父类中,让子类实现具体细节。
过度使用注释
大量不必要的注释,可能表明代码本身存在可读性和可理解性方面的问题。
可读性差:过多注释可能会掩盖代码逻辑,降低可读性。
维护成本高:随着代码的变更,注释也需要相应更新,从而增加了维护成本。
解决方案:
优化代码结构:
改进代码结构,使其更加清晰易懂,减少对注释的依赖。
示例:使用更具描述性的变量、函数和类名,使代码具有自解释性。
删除冗余注释
删除那些没有价值的不必要注释,重点保留那些真正有助于理解的注释。
示例:删除诸如
// 这个变量存储用户 ID
之类的注释,改为使用int userId;
。使用文档工具
利用文档工具自动生成必要的文档,减少手动注释的需求。
示例:使用诸如 Javadoc 或 Doxygen 这样的工具来生成类和方法文档,确保准确性和完整性。
代码重构的方法
搜索和替换方法 —— 包括搜索源文件中的特定字符串值并执行必要的修改。
缺乏上下文意识:不考虑编程语言的语法和语义,导致误报和错误的风险很高。
隐藏 bug:即使看起来操作成功了,也可能会引入隐藏 bug,特别是在复杂代码库中。
上下文感知方法 —— 允许重构工具理解编程语言的语义,并在决定什么可以重构之前对程序的源代码建立全面的理解。
减少错误:工具可以做出更准确的决策,最大限度减少错误。
增强可靠性:与搜索和替换方法相比,通常更可靠和高效。
实现
抽象语法树(AST):许多工具使用 AST 来解析和分析代码结构。
语义分析:工具执行语义分析来理解不同代码元素之间的关系。
代码重构工具
JDeodorant —— 一款 Eclipse 插件,能够自动识别并为 Java 源码中的常见代码缺陷提供重构建议。能检测诸如大型/臃肿类、特征厌恶、switch 语句/类型检查以及长方法等问题。
TrueRefactor —— 一款专为 Java 开发者设计的重构工具。包括重命名、提取方法和移动方法。会建议相应的重构操作。
Eclipse Refactoring —— Eclipse IDE 中针对 Java 项目的内置重构工具。包括重命名、提取方法和移动方法。对所有指向重构后的代码的引用进行更新,以降低出错风险。
IntelliJ IDEA Refactoring —— 一款功能强大的集成开发环境,为 Java 及其他语言提供了全面的重构功能。支持重命名、提取方法、移动方法以及提取接口功能。通过智能分析来执行重构操作。
Wrangler —— 一款用于在多种编程语言中进行代码克隆检测及重构的工具。识别并重构代码重复片段,支持多种语言。
RefactoringMiner —— 一个独立的库,用于在版本历史记录中检测出代码重构实例。99.6% 的准确率和 94% 的召回率。采用语句匹配算法来检测代码重构。
RefDiff —— 通过分析源代码中的变化来检测版本历史中的重构操作。通过比较代码差异来识别重构操作。支持多种操作类型。
使用重构工具的最佳实践
自动化单元测试 —— 在进行重构之前先设置自动化单元测试,以确保在进行更改后代码仍能按预期运行。
单代码库策略 —— 将所有项目存储在单一代码库中,以便能够安全且原子的对多个项目进行重构。
增量重构 —— 以小的、逐步的方式进行重构。
代码审查 —— 进行代码审查,以确保重构更改是适当的,并且不会引入新的问题。
结论
重构是一种持续进行的实践,通过使其更易于阅读、维护和扩展来提高代码质量。通过运用有策略的重构技术,开发人员能够减少技术债务,并保持代码库的整洁和高效。无论是处理小规模调整还是大规模重构,这些策略对软件质量的长期维护都有显著贡献。
你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
版权声明: 本文为 InfoQ 作者【俞凡】的原创文章。
原文链接:【http://xie.infoq.cn/article/749c97d91ed3f3bdedf0d211a】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论