一篇关于重构遗留代码的实用指南,内容涵盖识别、优先级排序、技术以及现代化和可维护性的最佳实践。
驯服猛兽:遗留代码重构策略
遗留代码。这个术语本身常常让人联想到庞大、无文档的系统、脆弱的依赖关系以及一种难以抗拒的恐惧感。全球许多开发人员都面临着维护和发展这些系统的挑战,而这些系统通常对业务运营至关重要。这份全面的指南提供了重构遗留代码的实用策略,将令人沮丧的源头转变为现代化和改进的机会。
什么是遗留代码?
在深入探讨重构技术之前,我们有必要先定义“遗留代码”的含义。虽然该术语可以简单地指代较旧的代码,但一个更细致的定义侧重于其可维护性。Michael Feathers 在其开创性著作《有效处理遗留代码》(Working Effectively with Legacy Code)中,将遗留代码定义为没有测试的代码。这种测试的缺乏使得在不引入回归问题的情况下安全地修改代码变得非常困难。然而,遗留代码也可能表现出其他特征:
- 文档缺失:原始开发人员可能已经离职,留下很少或根本没有文档来解释系统的架构、设计决策,甚至基本功能。
- 复杂的依赖关系:代码可能紧密耦合,使得在不影响系统其他部分的情况下难以隔离和修改单个组件。
- 技术过时:代码可能使用较旧的编程语言、框架或库编写,这些技术不再受积极支持,从而带来安全风险并限制了对现代工具的访问。
- 代码质量差:代码可能包含重复代码、长方法和其他“代码异味”,使其难以理解和维护。
- 脆弱的设计:看似微小的改动可能会产生无法预料且广泛的影响。
值得注意的是,遗留代码本身并不一定是坏事。它通常代表了一项重大投资,并体现了宝贵的领域知识。重构的目标是在保留这些价值的同时,提高代码的可维护性、可靠性和性能。
为什么要重构遗留代码?
重构遗留代码可能是一项艰巨的任务,但其好处往往大于挑战。以下是投资重构的一些关键原因:
- 提高可维护性:重构使代码更易于理解、修改和调试,从而降低了持续维护所需的成本和精力。对于全球团队而言,这一点尤为重要,因为它减少了对特定个人的依赖,并促进了知识共享。
- 减少技术债务:技术债务指的是因当前选择简单方案而非更耗时但更好的方法而产生的隐性重做成本。重构有助于偿还这笔债务,改善代码库的整体健康状况。
- 增强可靠性:通过解决代码异味和改进代码结构,重构可以降低错误的风险,提高系统的整体可靠性。
- 提升性能:重构可以识别和解决性能瓶颈,从而缩短执行时间并提高响应速度。
- 简化集成:重构可以使遗留系统更容易与新系统和技术集成,从而实现创新和现代化。例如,一个欧洲的电子商务平台可能需要与使用不同 API 的新支付网关集成。
- 提升开发者士气:使用整洁、结构良好的代码对开发者来说更愉快、更高效。重构可以提升士气并吸引人才。
识别重构候选对象
并非所有遗留代码都需要重构。根据以下因素确定重构工作的优先级至关重要:
- 变更频率:经常修改的代码是重构的主要候选对象,因为可维护性的提高将对开发效率产生重大影响。
- 复杂性:复杂且难以理解的代码更有可能包含错误,并且更难安全地修改。
- 错误的影响:对业务运营至关重要或有高风险导致昂贵错误的代码应优先重构。
- 性能瓶颈:被识别为性能瓶颈的代码应进行重构以提高性能。
- 代码异味:留意常见的代码异味,如长方法、大类、重复代码和特性依恋(feature envy)。这些都是可以从重构中受益的领域的指标。
示例:假设一家全球物流公司有一个管理货运的遗留系统。负责计算运费的模块由于法规和燃油价格的变化而频繁更新。该模块就是重构的首要候选对象。
重构技术
有许多可用的重构技术,每种技术都旨在解决特定的代码异味或改进代码的特定方面。以下是一些常用的技术:
组合方法 (Composing Methods)
这些技术专注于将大型、复杂的方法分解为更小、更易于管理的方法。这可以提高可读性,减少重复,并使代码更易于测试。
- 提炼方法 (Extract Method):这包括识别执行特定任务的代码块并将其移动到新方法中。
- 内联方法 (Inline Method):这包括用方法的主体替换方法调用。当方法名称与其主体一样清晰时,或者当您准备使用“提炼方法”但现有方法太短时,请使用此方法。
- 以查询取代临时变量 (Replace Temp with Query):这包括用一个按需计算变量值的方法调用来替换临时变量。
- 引入解释性变量 (Introduce Explaining Variable):用此方法将表达式的结果赋给一个具有描述性名称的变量,以阐明其目的。
在对象之间移动特性 (Moving Features Between Objects)
这些技术专注于通过将职责移动到它们所属的位置来改进类和对象的设计。
- 搬移方法 (Move Method):这包括将一个方法从一个类移动到它在逻辑上所属的另一个类。
- 搬移字段 (Move Field):这包括将一个字段从一个类移动到它在逻辑上所属的另一个类。
- 提炼类 (Extract Class):这包括从现有类中提取一组内聚的职责来创建一个新类。
- 内联类 (Inline Class):当一个类不再承担足够多的工作以证明其存在的合理性时,用此方法将其折叠到另一个类中。
- 隐藏委托关系 (Hide Delegate):这包括在服务器类中创建方法以向客户端隐藏委托逻辑,从而减少客户端和委托对象之间的耦合。
- 移除中间人 (Remove Middle Man):如果一个类几乎将其所有工作都委托出去,这有助于去除中间环节。
- 引入外加函数 (Introduce Foreign Method):在客户端类中添加一个方法,为客户端提供真正需要但由于无法访问或计划更改服务器类而无法修改的服务器类功能。
- 引入本地扩展 (Introduce Local Extension):创建一个包含新方法的新类。当您无法控制类的源代码并且不能直接添加行为时,这很有用。
组织数据 (Organizing Data)
这些技术专注于改进数据的存储和访问方式,使其更易于理解和修改。
- 以对象取代数据值 (Replace Data Value with Object):这包括用一个封装相关数据和行为的对象来替换一个简单的数据值。
- 将值对象改为引用对象 (Change Value to Reference):当多个对象共享相同的值时,这包括将值对象更改为引用对象。
- 将单向关联改为双向 (Change Unidirectional Association to Bidirectional):在两个只有一个单向链接的类之间创建一个双向链接。
- 将双向关联改为单向 (Change Bidirectional Association to Unidirectional):通过将双向关系变为单向来简化关联。
- 以符号常量取代魔法数 (Replace Magic Number with Symbolic Constant):这包括用命名常量替换字面值,使代码更易于理解和维护。
- 封装字段 (Encapsulate Field):为访问字段提供 getter 和 setter 方法。
- 封装集合 (Encapsulate Collection):确保对集合的所有更改都通过所有者类中严格控制的方法进行。
- 以数据类取代记录 (Replace Record with Data Class):创建一个新类,其字段与记录的结构和访问器方法相匹配。
- 以类取代类型码 (Replace Type Code with Class):当类型码具有一组有限的、已知可能值时,创建一个新类。
- 以子类取代类型码 (Replace Type Code with Subclasses):用于当类型码的值影响类的行为时。
- 以状态/策略模式取代类型码 (Replace Type Code with State/Strategy):用于当类型码的值影响类的行为,但子类化不适用时。
- 以字段取代子类 (Replace Subclass with Fields):移除一个子类,并在超类中添加代表子类独特属性的字段。
简化条件表达式 (Simplifying Conditional Expressions)
条件逻辑可能很快变得错综复杂。这些技术旨在澄清和简化它。
- 分解条件表达式 (Decompose Conditional):这包括将一个复杂的条件语句分解成更小、更易于管理的部分。
- 合并条件表达式 (Consolidate Conditional Expression):这包括将多个条件语句组合成一个更简洁的语句。
- 合并重复的条件片段 (Consolidate Duplicate Conditional Fragments):这包括将条件语句的多个分支中重复的代码移到条件语句之外。
- 移除控制标记 (Remove Control Flag):消除用于控制逻辑流程的布尔变量。
- 以卫语句取代嵌套条件表达式 (Replace Nested Conditional with Guard Clauses):通过将所有特殊情况放在顶部,并在任何一个为真时停止处理,使代码更具可读性。
- 以多态取代条件表达式 (Replace Conditional with Polymorphism):这包括用多态替换条件逻辑,允许不同的对象处理不同的情况。
- 引入空对象 (Introduce Null Object):创建一个提供默认行为的默认对象,而不是检查空值。
- 引入断言 (Introduce Assertion):通过创建一个检查期望的测试来明确地记录期望。
简化方法调用 (Simplifying Method Calls)
- 重命名方法 (Rename Method):这看起来很明显,但对于使代码清晰非常有帮助。
- 添加参数 (Add Parameter):向方法签名添加信息可以使方法更加灵活和可重用。
- 移除参数 (Remove Parameter):如果未使用某个参数,就把它去掉以简化接口。
- 将查询函数和修改函数分离 (Separate Query from Modifier):如果一个方法既改变状态又返回值,就把它分成两个不同的方法。
- 令函数携带参数 (Parameterize Method):用此方法将类似的方法合并为一个带有参数的方法,该参数可以改变行为。
- 以明确函数取代参数 (Replace Parameter with Explicit Methods):与参数化相反——将单个方法拆分为多个方法,每个方法代表参数的特定值。
- 保持对象完整 (Preserve Whole Object):不要向方法传递几个特定的数据项,而是传递整个对象,以便方法可以访问其所有数据。
- 以函数取代参数 (Replace Parameter with Method):如果一个方法总是以从字段派生的相同值被调用,可以考虑在方法内部派生参数值。
- 引入参数对象 (Introduce Parameter Object):当几个参数自然地属于一起时,将它们组合成一个对象。
- 移除设值方法 (Remove Setting Method):如果一个字段只应在构造后初始化而不应被修改,则避免使用 setter。
- 隐藏函数 (Hide Method):如果一个方法只在单个类中使用,则降低其可见性。
- 以工厂方法取代构造函数 (Replace Constructor with Factory Method):一种比构造函数更具描述性的替代方案。
- 以测试取代异常 (Replace Exception with Test):如果异常被用作流程控制,请用条件逻辑替换它们以提高性能。
处理泛化 (Dealing with Generalization)
- 字段上移 (Pull Up Field):将字段从子类移动到其超类。
- 函数上移 (Pull Up Method):将方法从子类移动到其超类。
- 构造函数本体上移 (Pull Up Constructor Body):将构造函数的主体从子类移动到其超类。
- 函数下移 (Push Down Method):将方法从超类移动到其子类。
- 字段下移 (Push Down Field):将字段从超类移动到其子类。
- 提炼接口 (Extract Interface):从类的公共方法创建一个接口。
- 提炼超类 (Extract Superclass):将两个类的共同功能移动到一个新的超类中。
- 折叠继承体系 (Collapse Hierarchy):将超类和子类合并为一个类。
- 塑造模板方法 (Form Template Method):在超类中创建一个模板方法,定义算法的步骤,允许子类重写特定步骤。
- 以委托取代继承 (Replace Inheritance with Delegation):在类中创建一个引用该功能的字段,而不是继承它。
- 以继承取代委托 (Replace Delegation with Inheritance):当委托过于复杂时,切换到继承。
这些只是众多可用重构技术中的几个例子。选择使用哪种技术取决于具体的代码异味和期望的结果。
示例:一家全球性银行使用的 Java 应用程序中有一个庞大的方法用于计算利率。应用提炼方法 (Extract Method) 来创建更小、更集中的方法,可以提高可读性,并使其更容易更新利率计算逻辑,而不会影响方法的其他部分。
重构过程
重构应系统地进行,以最大限度地降低风险并提高成功率。以下是推荐的流程:
- 识别重构候选对象:使用前面提到的标准来识别最能从重构中受益的代码区域。
- 创建测试:在进行任何更改之前,编写自动化测试来验证代码的现有行为。这对于确保重构不引入回归至关重要。可以使用 JUnit (Java)、pytest (Python) 或 Jest (JavaScript) 等工具编写单元测试。
- 增量重构:进行小的、增量的更改,并在每次更改后运行测试。这使得更容易识别和修复任何引入的错误。
- 频繁提交:频繁地将您的更改提交到版本控制。如果出现问题,这使您可以轻松地恢复到以前的版本。
- 代码审查:让另一位开发人员审查您的代码。这有助于识别潜在问题并确保重构正确完成。
- 监控性能:重构后,监控系统的性能,以确保更改没有引入任何性能回归。
示例:一个团队在重构一个全球电子商务平台的 Python 模块时,使用 `pytest` 为现有功能创建单元测试。然后他们应用提炼类 (Extract Class) 重构来分离关注点并改善模块的结构。在每次小的更改后,他们都会运行测试以确保功能保持不变。
为遗留代码引入测试的策略
正如 Michael Feathers 所恰当指出的,遗留代码是没有测试的代码。向现有代码库引入测试感觉像是一项巨大的任务,但对于安全重构至关重要。以下是完成此任务的几种策略:
特征化测试 (Characterization Tests) (又名黄金标准测试)
当您处理难以理解的代码时,特征化测试可以帮助您在开始进行更改之前捕获其现有行为。其思想是编写测试,断言代码对于一组给定的输入所产生的当前输出。这些测试不一定验证正确性;它们只是记录代码*当前*的行为。
步骤:
- 识别您想要特征化的代码单元(例如,一个函数或方法)。
- 创建一组代表常见和边缘情况场景的输入值。
- 用这些输入运行代码并捕获结果输出。
- 编写测试,断言代码为这些输入产生了那些确切的输出。
注意:如果底层逻辑复杂或依赖于数据,特征化测试可能会很脆弱。如果您以后需要更改代码的行为,请准备好更新它们。
萌芽方法 (Sprout Method) 和萌芽类 (Sprout Class)
这些技术同样由 Michael Feathers 描述,旨在将新功能引入遗留系统,同时最大限度地降低破坏现有代码的风险。
萌芽方法 (Sprout Method):当您需要添加一个需要修改现有方法的新功能时,创建一个包含新逻辑的新方法。然后,从现有方法中调用这个新方法。这使您能够隔离新代码并独立测试它。
萌芽类 (Sprout Class):与萌芽方法类似,但用于类。创建一个实现新功能的新类,然后将其集成到现有系统中。
沙盒化 (Sandboxing)
沙盒化涉及将遗留代码与系统的其余部分隔离开来,让您可以在受控环境中对其进行测试。这可以通过为依赖项创建模拟(mock)或存根(stub)或在虚拟机中运行代码来完成。
Mikado 方法
Mikado 方法是一种用于处理复杂重构任务的可视化问题解决方法。它涉及创建一个图表,表示代码不同部分之间的依赖关系,然后以最小化对系统其他部分影响的方式重构代码。其核心原则是“尝试”更改,看看会破坏什么。如果它被破坏了,就恢复到最后一个工作状态并记录问题。然后在重新尝试原始更改之前解决该问题。
重构工具
有几种工具可以协助重构,自动化重复性任务并提供最佳实践指导。这些工具通常集成到集成开发环境 (IDE) 中:
- IDE(例如,IntelliJ IDEA、Eclipse、Visual Studio):IDE 提供内置的重构工具,可以自动执行重命名变量、提取方法和移动类等任务。
- 静态分析工具(例如,SonarQube、Checkstyle、PMD):这些工具分析代码中的代码异味、潜在错误和安全漏洞。它们可以帮助识别可以从重构中受益的代码区域。
- 代码覆盖率工具(例如,JaCoCo、Cobertura):这些工具衡量测试覆盖的代码百分比。它们可以帮助识别未充分测试的代码区域。
- 重构浏览器(例如,Smalltalk Refactoring Browser):专门用于协助更大规模重组活动的工具。
示例:一个为全球保险公司开发 C# 应用程序的开发团队使用 Visual Studio 的内置重构工具来自动重命名变量和提取方法。他们还使用 SonarQube 来识别代码异味和潜在漏洞。
挑战与风险
重构遗留代码并非没有挑战和风险:
- 引入回归:最大的风险是在重构过程中引入错误。这可以通过编写全面的测试和增量重构来缓解。
- 缺乏领域知识:如果原始开发人员已经离职,可能很难理解代码及其用途。这可能导致不正确的重构决策。
- 紧密耦合:紧密耦合的代码更难重构,因为对一部分代码的更改可能会对代码的其他部分产生意想不到的后果。
- 时间限制:重构可能需要时间,并且可能难以向专注于交付新功能的利益相关者证明投资的合理性。
- 抵制变革:一些开发人员可能抵制重构,特别是如果他们不熟悉所涉及的技术。
最佳实践
为了减轻与重构遗留代码相关的挑战和风险,请遵循以下最佳实践:
- 获得支持:确保利益相关者了解重构的好处,并愿意投入所需的时间和资源。
- 从小处着手:从重构小的、孤立的代码块开始。这将有助于建立信心并展示重构的价值。
- 增量重构:进行小的、增量的更改并频繁测试。这将使识别和修复任何引入的错误变得更容易。
- 自动化测试:编写全面的自动化测试,以验证重构前后代码的行为。
- 使用重构工具:利用 IDE 或其他工具中可用的重构工具来自动化重复性任务并提供最佳实践指导。
- 记录您的更改:记录您在重构期间所做的更改。这将帮助其他开发人员理解代码并避免在未来引入回归。
- 持续重构:将重构作为开发过程的持续一部分,而不是一次性事件。这将有助于保持代码库的整洁和可维护性。
结论
重构遗留代码是一项具有挑战性但回报丰厚的工作。通过遵循本指南中概述的策略和最佳实践,您可以驯服这头猛兽,将您的遗留系统转变为可维护、可靠和高性能的资产。记住要系统地进行重构,频繁测试,并与您的团队有效沟通。通过周密的计划和执行,您可以释放遗留代码中隐藏的潜力,为未来的创新铺平道路。