代码重构专题文章
@[TOC]
前言
在软件开发的旅程中,代码的生命周期远不止于“能跑就行”。随着业务发展和需求迭代,我们常常会发现,当初为了快速实现功能而堆砌的代码,逐渐变成了难以理解和维护的“技术债”。这时,重构便成了我们手中的利剑。
重构:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。本质上说,重构就是在代码写好之后改进它的设计。
重构并非一蹴而就的魔法,而是一种持续、渐进的改进过程。它教会我们以微小的步伐修改程序,并在每次修改后运行测试,确保代码始终处于可工作状态。正如 Kent Beck 所提出的“两顶帽子”理论:在开发软件时,我们戴上“添加新功能”的帽子,专注于实现业务需求;戴上“重构”的帽子,则只关注代码结构的优化,绝不添加新功能。这种分而治之的策略,能让我们在保证稳定性的同时,不断提升代码质量。
一、重构,第一个示例
为什么需要重构?
因为 需求变化 是不可避免的。
- 如果要给程序添加一个新特性,但发现代码因缺乏良好的结构而难以下手,那么先重构,使代码更容易修改,再实现新功能。
- 重构的精髓是 小步快跑:以微小的步伐修改程序,并且每次修改后立刻运行测试,确保系统随时可用。
这样可以避免陷入“写了一堆代码,却不知道是不是还能跑”的窘境。
二、重构的原则
🎩 两顶帽子
Kent Beck 提出「两顶帽子」的比喻:
- 戴上 “添加功能” 的帽子时,只管写新功能,不改动旧逻辑。
- 戴上 “重构” 的帽子时,不加新功能,只调整已有代码结构。
这能帮我们保持清晰的开发节奏,避免两种行为混在一起。
💡 为何重构?
- 改进软件设计,使其更健壮和灵活。
- 让代码更容易理解。
- 帮助发现和修复潜在的 Bug。
- 提高开发效率,让未来的功能开发更快。
⏰ 何时重构?
- 添加新功能之前。
- 遇到糟糕的代码逻辑或命名时。
- “顺手捡垃圾”式的重构,每次经过时顺便改进一点。
- 有计划的大规模重构。
- 长期逐步演进的重构(每次进入“重构区”都推进一点)。
- Code Review 时。
- ⚠️ 不要和经理说“我要重构”,而是说“我要让代码更容易加功能”。
- 不需要动的代码不要动,如果重写比重构更简单,也不要死磕。
⚔️ 重构的挑战
-
普遍认为“延缓新功能开发”: 需要与团队和管理层沟通,强调长期收益。
-
受限于代码所有权: 跨团队协作时需明确职责。
-
分支集成回主线困难: 引入持续集成(CI)实践,通过“基于主干开发”减少合并冲突。
持续集成(Continuous Integration,CI),也叫“基于主干开发”(Trunk-Based Development)。在使用 CI 时,每个团队成员每天至少向主线集成一次。这个实践避免了任何分支彼此差异太大,从而极大地降低了合并的难度。
-
“自测试的代码”要求高: 虽然挑战,但却是重构成功的重要基石。
-
遗留代码面临没测试等窘境: 可以通过编写少量高价值的端到端测试来逐步建立信心,然后逐步添加单元测试。
-
数据库的重构: 相比代码,数据库重构更为复杂,通常需要将多个小修改串联起来,并考虑数据迁移策略。
-
三大实践协同:自测试代码 + 持续集成 + 重构。
-
重构优先发生在“高频使用”的代码上。
三、代码的坏味道(Code Smells)
好的代码是写出来的,更是重构出来的。识别代码中的“坏味道”是重构的第一步。以下是一些常见的“坏味道”,它们是代码质量下降的警示信号:
以下是常见的 24 种坏味道及对应的解决思路:
-
神秘命名(Mysterious Name): 变量、函数、类名晦涩难懂,无法直观表达其用途。
-
重复代码(Duplicated Code): 相同或相似的代码块在多处出现。
-
过长函数(Long Function): 函数包含过多逻辑,难以理解和维护。
-
过长参数列表(Long Parameter List): 函数参数过多,增加调用难度和出错概率。
-
全局数据(Global Data)
-
可变数据(Mutable Data): 全局变量和频繁变更的数据,导致难以追踪和控制状态。
-
发散式变化(Divergent Change): 某个类因不同原因而在不同方向上发生变化。
-
霰弹式修改(Shotgun Surgery): 一种变化需要在多个类中进行许多小修改。
-
依恋情结(Feature Envy): 函数对其他模块的数据或函数过于“依恋”,频繁交互。
-
数据泥团(Data Clumps): 相同的三四项数据总是结伴出现(例如在多个函数签名中)。
重构建议: 运用**提炼类(Extract Class)**将它们提炼到一个独立对象中。
-
基本类型偏执(Primitive Obsession): 过于依赖基本类型(如字符串、整数)来表示领域概念,而非创建小对象(如
Money、Coordinate、Range)。 -
重复的 Switch(Repeated Switches): 相同或类似的
switch/case结构散布在代码库中。重构建议: 考虑多态来替代
switch。 -
循环语句(Loops): 传统
for/while循环可能使代码意图模糊。重构建议: 优先使用函数式编程中的 Stream API 或其他高阶函数,使代码更简洁、表达力更强。
-
冗赘的元素(Lazy Element): 类、函数等结构看似提供抽象,实则无用,徒增复杂度。
-
夸夸其谈通用性(Speculative Generality): 过度设计、引入了目前用不上的通用功能,反而增加了维护成本。
-
临时字段(Temporary Field): 某些字段只在特定情况下被赋值和使用。
-
过长的消息链(Message Chains):
objA.getObjB().getObjC().getSomeProperty(),这种链式调用使得代码对深层结构过于依赖。 -
中间人(Middle Man): 类通过委托将所有工作转发给另一个类,自身没有实质性功能。
-
内幕交易(Insider Trading): 模块之间过度了解彼此的内部实现细节,导致紧密耦合。
-
过大的类(Large Class): 类承担了过多职责,拥有过多的字段和方法。
-
异曲同工的类(Alternative Classes with Different Interfaces): 两个类实现相似的功能,但接口不一致。
-
纯数据类(Data Class): 只有字段和访问器(getter/setter),缺少行为。
重构建议: 纯数据类往往意味着行为被放置在了错误的地方,应考虑将相关行为移入数据类。
-
被拒绝的遗赠(Refused Bequest): 子类不需要或不希望继承超类的某些方法或数据。
-
注释(Comments): 存在大量解释性注释,往往是因为代码本身不够清晰易懂。
重构建议: 好的代码应该是自解释的,尽量用代码本身表达意图,减少不必要的注释。
四、构筑测试体系
✅ 为什么需要测试?
- 自动化测试可以极大提高开发速度。
- 测试的重点应该放在“最担心出错的部分”。
- 一旦测试运行通过,就能大胆重构。
🕒 何时写测试?
- 在写功能代码之前先写测试(TDD 思路)。
- 这样能让注意力集中在 接口设计 而非实现细节。
🧰 最佳实践
- 测试应该是自验证的(自测试代码)。
- 测试执行应该像编译一样简单,最好能在每次构建时自动运行。
- 避免共享测试夹具,尽量保持独立性。
五、重构目录结构
为了更好地实践重构,我们需要一套标准化的方法论。通常,一份重构技术指南会包含以下几个关键部分:
| 组成部分 | 简介 | 备注 |
|---|---|---|
| 名称(name) | 同时列出常见的别名,方便查找 | |
| 速写(sketch) | 简要描述重构手法,帮助快速理解 | 核心思想的概括 |
| 动机(motivation) | “为什么需要做这个重构”和“什么情况下不该做这个重构” | 明确重构的适用场景和价值 |
| 做法(mechanics) | 如何一步步进行此重构,通常包含具体的操作步骤 | 可操作性指导 |
| 范例(examples) | 以一个十分简单的例子说明此重构手法如何运作 | 实际应用演示 |
展望未来
重构是一场永无止境的旅程,它贯穿于软件开发的整个生命周期。通过持续的重构,我们不仅能写出更高质量的代码,更能培养一种精益求精的工匠精神。希望这篇笔记能为你揭开重构的神秘面纱,助你在代码的世界里,化腐朽为神奇!
TODO: 后面还有详细的重构方法待学习