第1章:遗留代码基础(Legacy Code Fundamentals)
一、导读
1.1 本章学习目标
第1章定义了什么是遗留代码,以及面对遗留代码时的基本策略。Michael Feathers 给出了一个颠覆性的定义:遗留代码就是没有测试的代码。
本章的核心学习目标包括:
- 理解遗留代码的真正含义:没有测试 = 遗留代码
- 掌握修改遗留代码的基本策略
- 理解"修改困境"
- 学会建立测试安全网
1.2 为什么本章如此重要
大多数开发者大部分时间都在与遗留代码打交道。Feathers 的定义改变了我们看待遗留代码的方式——问题不在于代码有多旧,而在于我们能否安全地修改它。
1.3 核心问题引导
为什么"没有测试"是遗留代码的核心问题?
修改遗留代码的困境是什么?
如何在不理解代码的情况下安全地修改它?
特征测试和回归测试有什么区别?
二、核心概念详解
2.1 遗留代码的定义
传统定义:遗留代码 = 旧的、过时的代码
Feathers 定义:遗留代码 = 没有测试的代码
关键洞察:
- 有测试的旧代码不是遗留代码(可以安全修改)
- 没有测试的新代码是遗留代码(无法安全修改)
- 测试是修改的安全网2.2 修改困境
修改困境(Change Dilemma):
1. 要修改代码,你需要理解它
2. 要理解它,你需要修改它
3. 修改它可能导致 bug
4. 没有测试,你不知道是否引入了 bug
解决方案:先添加测试,再修改代码2.3 安全修改的步骤
遗留代码修改算法:
1. 识别变更点(在哪里需要修改?)
2. 找到测试点(在哪里可以插入测试?)
3. 分解依赖(如何让代码可以被测试?)
4. 编写测试(建立安全网)
5. 修改代码(在测试保护下修改)
6. 运行测试(确认没有破坏)2.4 特征测试
java
// 特征测试:记录系统的实际行为
// 即使行为看起来是"错误的",也要先记录下来
@Test
void testCurrentBehavior() {
// 不关心是否正确,只关心当前行为是什么
String result = legacySystem.process("input");
assertEquals("actual_output", result); // 记录实际输出
}
// 有了特征测试后,你可以安全地重构
// 如果重构后测试仍然通过,说明行为没有改变三、实战案例
3.1 案例一:为遗留代码添加第一个测试
java
// 遗留代码
public class Payroll {
public int calculatePay(Employee employee) {
int base = employee.getBaseSalary();
int bonus = 0;
if (employee.getYears() > 5) bonus = base / 10;
if (employee.getDepartment().equals("Sales")) bonus += 1000;
// 200 行复杂的计算逻辑...
return base + bonus;
}
}
// 第一步:写特征测试
@Test
void testCalculatePayKnownCase() {
Employee emp = new Employee("Engineering", 3, 50000);
Payroll payroll = new Payroll();
int pay = payroll.calculatePay(emp);
assertEquals(50000, pay); // 记录实际行为
}
// 第二步:在测试保护下重构
// 现在可以安全地提取方法、重命名变量、简化逻辑四、本章小结
遗留代码 = 没有测试的代码。
修改困境:要修改需要理解,要理解需要修改。
特征测试:记录系统实际行为,作为安全网。
安全修改:先添加测试,再修改代码。
五、思考与练习
为什么 Feathers 将遗留代码定义为"没有测试的代码"?
特征测试和单元测试有什么区别?
为一个你熟悉的遗留系统制定修改策略。