写点什么

代码重构指南

作者:俞凡
  • 2025-09-08
    上海
  • 本文字数:5468 字

    阅读完需:约 18 分钟

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


简介

重构是在不改变代码外部行为的情况下改进代码内部架构的过程,旨在增强代码可读性、降低复杂性并提高可维护性。本指南全面介绍了重构的艺术,涵盖了有效管理代码变更的基本概念、类型、目的、挑战和特定技巧。

重构的基本概念

  • 重构是修改代码以改进其内部架构,而不改变其外部行为。

重构的类型

1. 小型重构 —— 仅限于单个类

  • 变量重命名:使变量名称更具描述性,例如,将 i 更改为 interstRate

  • 提取变量:将重复表达式放到到单个变量中。

  • 提取函数:将重复代码块放入单独的函数中,以便更好的重用。

2. 中型重构 —— 跨多个类

  • 提取接口:将常见方法组合成多态接口。

  • 提取父类:将共享的属性和方法移动到公共父类中。

  • 委派:将职责从一个类分配到另一个类,以优化关注点分离。

3. 大规模重构 —— 在系统组件级别。

  • 拆分/合并服务:通过拆分或合并服务来优化系统架构。

  • 组件化:将系统分解为独立的组件,以提高模块化和可维护性。

重构的目的

  • 重构的目的是创建更合乎逻辑的代码架构,增强可读性,降低复杂性,并改善可维护性。

  • 简化代码,使其更容易扩展并适应未来需求,在添加新功能时能最大限度减少复杂性。

  • 增量重构以最小化与大规模变更相关的风险。每次修改后都要进行彻底测试,以确保稳定性。

具体措施

  • 删除重复代码,使代码更简洁和可维护。

  • 为变量和函数使用更具描述性的名称以提高清晰度。

  • 通过提取可重用函数和优化方法参数来分解复杂逻辑。

  • 将代码分解为独立、可重用的模块。

  • 明确定义接口以减少模块耦合。

  • 应用适当的模式来增加灵活性。

  • 随着时间推移,做一些小的、可管理的改变,以积累更大的进步。

  • 实现单元测试、功能测试和回归测试,以验证更改后的行为。

  • 在生产环境中逐步推出更改,以尽量减少对用户的影响。

何时重构

  • Don Roberts 的“三遍即重构(three strikes and refactor)”原则指出,当一段代码重复出现三次时,就应该对其进行重构,以避免冗余。

  • 将相似代码重构为抽象方法、优化变量并删除冗余逻辑。

  • 在添加新功能时,对现有代码进行小规模重构以保持一致性。

  • 代码审查可从经验丰富的同事那里获得见解,有助于知识传递并提出改进建议。

  • 对于中大型重构,将其纳入需求中。对于大规模变更,制定详细计划并评估影响。

  • 当出现线上问题时,就是进行改进的好时机。

何时不应进行重构

  • 如果由于过于复杂而导致重写比重构更简单,那么重写可能是更好的选择。

  • 稳定性高且很少修改的代码可能无需进行重构。

重构的挑战

  • 困难的重构可能需要时间,从而影响进度。业务支持至关重要。

  • 重构可能需要在旧代码和新代码中同时实现新功能。

  • 不完整的重构会混合旧代码和新代码的逻辑,增加理解代码的成本。

  • 由于复杂性导致的核心业务逻辑或数据库架构可能难以重构。

  • 影响接口的更改可能会破坏其他系统,需要谨慎管理。

  • 长期影响需要进行彻底研究和跟踪。

控制重构风险

  • 使用 IDE 重构工具:使用现代工具确保安全的重构。

  • 自动化测试:进行彻底测试,使重构后的代码的行为与原始代码保持一致。

  • 流量回放测试:在真实环境中验证性能。

  • 灰盒部署:逐步部署更改,以尽量减少对用户的影响。

  • 监控警报:迅速识别并解决问题。

涉及数据库和数据结构的重构

  • 数据库结构变更需要进行精心规划,以确保数据的完整性和一致性。

  • 迁移计划:制定平稳的数据转换方案。

  • 回滚策略:制定回滚策略,以便在出现问题时能够迅速恢复。

用例

  • 在添加新功能的过程中进行重构能够使代码结构更加清晰。

  • 应优先解决 bug,然后再进行重构。

  • 定期审查有助于发现并消除“代码异味”。

  • 重构是敏捷方法中用于持续提高代码质量的核心活动。

常见重构场景

  1. 长参数


如果一个方法有过多参数,那么调用该方法就会容易出错,并且会增加复杂度和维护成本。


当方法签名中的参数超过 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 方法。


  1. 简化条件逻辑


大量使用条件语句的方法(具有多重嵌套 if-else 语句或过于复杂的条件语句的方法)难以理解和维护。


解决方案:


  • 提取方法:将复杂条件逻辑移至单独的方法中,使主方法更加简洁。

  • 示例:对于包含嵌套 if-else 语句的方法 void processOrder(Order order),将其重构为 void processOrder(Order order) { if (isValidOrder(order)) { ... } },其中 isValidOrder 包含所有条件逻辑。

  • 使用策略模式

  • 策略模式:将不同条件逻辑封装到不同策略类中,并根据条件在主方法中切换。

  • 示例:定义 OrderProcessingStrategy 接口,并实现诸如 StandardOrderProcessingStrategySpecialOrderProcessingStrategy 等实现类,然后在 processOrder 方法中使用。

  • 运用多态性

  • 多态性:通过继承和多态,将不同的条件逻辑分配到不同的子类中。

  • 示例:定义 OrderProcessor 抽象类,并派生出 StandardOrderProcessorSpecialOrderProcessor,在主方法中根据条件选择合适的处理类。


  1. 分散修改导致高昂的维护成本


当一个功能的实现分散在多个类或方法中时,任何修改都会影响到很多地方,从而增加了维护成本。


实现代码分散在多个文件中,因此每次修改都需要在多个地方进行查找和修改。


解决方案:


  • 提取类

  • 将分散的功能整合到一个单一的类中,使功能更加集中且更易于管理。

  • 示例:假设订单处理的功能分布在 OrderServicePaymentServiceInventoryService 等多个类中。创建 OrderProcessor 类来整合这些功能。

  • 使用委托

  • 职责分离:将某些功能委托给其他类,从而减轻主类的职责。

  • 示例:将订单验证委托给 OrderValidator 类,将支付处理委托给 PaymentHandler 类。

  • 运用设计模式

  • 责任链模式:将多个处理步骤串联起来,每个步骤负责处理功能的一部分。

  • 示例:创建 OrderProcessingChain 类,将订单验证、支付处理和库存检查串联起来,每个步骤由单独的类来管理。


  1. 长函数


如果函数代码过长(超过 20 行的函数,尤其是那些包含多个子任务的函数),就会变得难以理解和维护。


解决方案:


  • 提取方法

  • 将子任务拆分为独立的方法,使主函数更加简洁。

  • 示例:对于包含请求解析、数据处理和结果返回的 void processUserRequest(UserRequest request) 方法,将其重构为 void processUserRequest(UserRequest request) { parseRequest(request); processData(); returnResult();}}.

  • 使用类

  • 创建类:将多个子任务封装到单独的类中,使代码更具模块化。

  • 示例:创建 UserRequestProcessor 类,以封装 parseRequestprocessDatareturnResult 等方法。

  • 运用设计模式

  • 模板方法模式:通过使用模板方法模式,将常见处理步骤提取到父类中,让子类负责具体实现。

  • 示例:创建 AbstractRequestProcessor 类,定义常见处理步骤,并创建 UserRequestProcessor 子类来负责具体实现。


  1. 死代码


死代码指的是那些永远不会被执行的代码(未使用的变量、未被调用的方法、永远不会被执行的分支),通常是因为逻辑错误或遗留问题所致。


解决方案:


  • 静态分析

  • 使用诸如 SonarQube 或 IntelliJ IDEA 之类的静态分析工具来识别并删除无用代码。

  • 示例:删除未使用的变量,例如 int unusedVariable = 0; 或无用的方法,例如 void unusedMethod() { ... }.

  • 简化逻辑

  • 简化条件检查:移除那些永远不会执行的分支,例如:if (false) { ... }}

  • 示例:删除 if (false) { ... } 这样的分支,因为该分支永远不会被执行。

  • 重构旧代码

  • 清理历史代码:在系统维护期间逐步清理过时的代码段。

  • 示例:在系统升级过程中删除诸如 void oldMethod() { ... } 这样的旧版本方法。


  1. 大型类


一个类试图承担过多的任务,从而导致代码冗长、复杂且难以理解。


  • 该类包含的代码量过多,导致难以阅读和理解。

  • 该类包含多个不相关的功能,使内部逻辑变得复杂。

  • 该类与其他类高度耦合,这意味着对一个类的更改可能会影响到多个其他类。


解决方案:


  • 提取类

  • 将类分解为更小、更专注的子类,减少每个类的职责范围。

  • 示例:一个包含订单处理、支付处理、库存检查等功能的 OrderProcessor 类,可以拆分为 OrderServicePaymentServiceInventoryService 等子类,每个子类分别负责一项特定功能。

  • 运用设计模式

  • 应用诸如责任链模式或策略模式这样的模式来分配类的职责。

  • 示例:使用责任链模式将订单处理的不同步骤(如验证、支付、库存)串联起来,由不同的类分别处理每个步骤。


  1. 重复代码


程序中存在过多重复代码,会增加冗余和系统间的耦合性,从而降低可维护性和稳定性。


  • 重复代码会增加代码的长度,并降低可维护性。

  • 重复代码会增加系统间的耦合度,因此在一处进行的更改往往需要在多处进行调整。

  • 重复代码会增加潜在的错误点,并降低系统的稳定性。


解决方案:


  • 提取方法

  • 将重复代码提取到单独的方法中,以提高可复用性。

  • 示例:如果在多个地方存在相同的订单验证逻辑,将其提取到一个名为 isValidOrder(Order order) 的方法中。

  • 使用继承或委托

  • 通过使用继承或委托来减少重复代码。

  • 示例:将通用的订单处理逻辑放在基类中,让子类实现特定的逻辑。

  • 使用设计模式

  • 应用诸如模板方法或工厂模式这样的模式来减少重复。

  • 示例:使用模板方法模式将常见处理步骤提取到父类中,让子类实现具体细节。


  1. 过度使用注释


大量不必要的注释,可能表明代码本身存在可读性和可理解性方面的问题。


  • 可读性差:过多注释可能会掩盖代码逻辑,降低可读性。

  • 维护成本高:随着代码的变更,注释也需要相应更新,从而增加了维护成本。


解决方案:


  • 优化代码结构:

  • 改进代码结构,使其更加清晰易懂,减少对注释的依赖。

  • 示例:使用更具描述性的变量、函数和类名,使代码具有自解释性。

  • 删除冗余注释

  • 删除那些没有价值的不必要注释,重点保留那些真正有助于理解的注释。

  • 示例:删除诸如 // 这个变量存储用户 ID 之类的注释,改为使用 int userId;

  • 使用文档工具

  • 利用文档工具自动生成必要的文档,减少手动注释的需求。

  • 示例:使用诸如 Javadoc 或 Doxygen 这样的工具来生成类和方法文档,确保准确性和完整性。

代码重构的方法

  1. 搜索和替换方法 —— 包括搜索源文件中的特定字符串值并执行必要的修改。


  • 缺乏上下文意识:不考虑编程语言的语法和语义,导致误报和错误的风险很高。

  • 隐藏 bug:即使看起来操作成功了,也可能会引入隐藏 bug,特别是在复杂代码库中。


  1. 上下文感知方法 —— 允许重构工具理解编程语言的语义,并在决定什么可以重构之前对程序的源代码建立全面的理解。


  • 减少错误:工具可以做出更准确的决策,最大限度减少错误。

  • 增强可靠性:与搜索和替换方法相比,通常更可靠和高效。


实现


  • 抽象语法树(AST):许多工具使用 AST 来解析和分析代码结构。

  • 语义分析:工具执行语义分析来理解不同代码元素之间的关系。

代码重构工具

  1. JDeodorant —— 一款 Eclipse 插件,能够自动识别并为 Java 源码中的常见代码缺陷提供重构建议。能检测诸如大型/臃肿类、特征厌恶、switch 语句/类型检查以及长方法等问题。

  2. TrueRefactor —— 一款专为 Java 开发者设计的重构工具。包括重命名、提取方法和移动方法。会建议相应的重构操作。

  3. Eclipse Refactoring —— Eclipse IDE 中针对 Java 项目的内置重构工具。包括重命名、提取方法和移动方法。对所有指向重构后的代码的引用进行更新,以降低出错风险。

  4. IntelliJ IDEA Refactoring —— 一款功能强大的集成开发环境,为 Java 及其他语言提供了全面的重构功能。支持重命名、提取方法、移动方法以及提取接口功能。通过智能分析来执行重构操作。

  5. Wrangler —— 一款用于在多种编程语言中进行代码克隆检测及重构的工具。识别并重构代码重复片段,支持多种语言。

  6. RefactoringMiner —— 一个独立的库,用于在版本历史记录中检测出代码重构实例。99.6% 的准确率和 94% 的召回率。采用语句匹配算法来检测代码重构。

  7. RefDiff —— 通过分析源代码中的变化来检测版本历史中的重构操作。通过比较代码差异来识别重构操作。支持多种操作类型。


使用重构工具的最佳实践


  • 自动化单元测试 —— 在进行重构之前先设置自动化单元测试,以确保在进行更改后代码仍能按预期运行。

  • 单代码库策略 —— 将所有项目存储在单一代码库中,以便能够安全且原子的对多个项目进行重构。

  • 增量重构 —— 以小的、逐步的方式进行重构。

  • 代码审查 —— 进行代码审查,以确保重构更改是适当的,并且不会引入新的问题。

结论

重构是一种持续进行的实践,通过使其更易于阅读、维护和扩展来提高代码质量。通过运用有策略的重构技术,开发人员能够减少技术债务,并保持代码库的整洁和高效。无论是处理小规模调整还是大规模重构,这些策略对软件质量的长期维护都有显著贡献。




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 4
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
代码重构指南_架构_俞凡_InfoQ写作社区