26、遗留系统重构(上):理解遗留代码、给遗留代码添加测试、安全的重构手法。

说实话,每个程序员职业生涯里,迟早都会撞上遗留系统。我见过太多团队,一听到“遗留代码”四个字就头疼。其实我也一样,刚入行那会儿,看到几千行没有注释的函数,心里直打鼓。

但后来我明白了。遗留代码不是垃圾,它是业务沉淀下来的财富。只是它需要我们用正确的方式去对待。今天我们就聊聊,怎么安全地跟遗留代码打交道。

什么是遗留代码?

我个人对遗留代码的定义很简单:没有测试的代码,就是遗留代码。不管它写得有多漂亮,只要没人敢改,它就是遗留代码。

为什么会这样?你想想看,一段代码如果没有测试保护,你改了一行,可能引发十个地方出问题。没人敢动,最后就变成了“祖传代码”。

我在项目中遇到过一套支付系统,核心逻辑写了快十年。代码风格很老,但业务逻辑极其复杂。没人敢重构,因为没人知道改了之后会不会少收钱。这就是典型的遗留系统困境。

核心观点:遗留代码 ≠ 坏代码。它只是缺少测试保护,导致我们不敢修改它。

给遗留代码添加测试

好,既然问题出在“没有测试”,那我们就给它加上测试。但这里有个坑——遗留代码往往很难测试

为什么?因为遗留代码通常耦合严重。一个函数里可能同时做了数据库查询、文件读写、业务计算、日志输出。你想单独测业务逻辑?对不起,你得先启动数据库。

那怎么办?我建议分三步走:

  1. 识别可测试的边界——找到那些输入输出明确的函数或模块
  2. 编写“特性测试”——不关心内部实现,只验证输入输出
  3. 逐步解耦——用测试倒逼代码改进

举个例子,假设我们有这么一段遗留代码:

public class OrderProcessor {
    public void process(Order order) {
        // 直接操作数据库
        Database.save(order);
        // 发送邮件
        EmailService.send(order.getCustomerEmail(), "订单已处理");
        // 计算折扣
        double discount = order.getTotal() * 0.1;
        order.setDiscount(discount);
    }
}

这段代码很难测。因为每次调用都会真的写数据库、发邮件。我们怎么给它加测试?

我的做法是:先写一个“特性测试”,只验证最终结果。

@Test
public void testProcessOrder() {
    Order order = new Order();
    order.setTotal(100.0);
    order.setCustomerEmail("test@test.com");
    
    OrderProcessor processor = new OrderProcessor();
    processor.process(order);
    
    // 验证折扣是否正确计算
    assertEquals(10.0, order.getDiscount(), 0.001);
}

这个测试虽然会真的写数据库、发邮件,但它至少验证了核心逻辑。等我们后续把数据库和邮件服务抽离成接口后,再替换成 mock 对象。

小技巧:给遗留代码加测试时,不要追求完美。先加一个能跑的测试,比加一个完美的测试更重要。我曾经花了两周想给一个模块写完美的单元测试,结果发现根本跑不通。后来改成先写集成测试,反而更快。

安全的重构手法

有了测试保护,我们就可以开始重构了。但重构遗留代码,最怕的就是“改着改着就崩了”。我总结了几种安全的手法:

1. 提取方法(Extract Method)

这是最安全、最常用的手法。把一段逻辑独立成方法,不改任何行为。

// 重构前
public void process(Order order) {
    double discount = order.getTotal() * 0.1;
    if (order.getTotal() > 1000) {
        discount = order.getTotal() * 0.2;
    }
    order.setDiscount(discount);
    Database.save(order);
}

// 重构后
public void process(Order order) {
    double discount = calculateDiscount(order);
    order.setDiscount(discount);
    Database.save(order);
}

private double calculateDiscount(Order order) {
    double discount = order.getTotal() * 0.1;
    if (order.getTotal() > 1000) {
        discount = order.getTotal() * 0.2;
    }
    return discount;
}

你看,逻辑完全没变,只是把折扣计算抽出来了。这种重构几乎零风险。

2. 引入参数对象

当一个方法有太多参数时,可以考虑把它们封装成一个对象。我在项目中遇到过这样一个方法:

public void createReport(String name, String type, Date start, Date end, boolean includeDetails) {
    // 30行代码
}

调用的时候,参数顺序一错就出问题。我把它改成了:

public void createReport(ReportRequest request) {
    // 30行代码
}

// 调用方
ReportRequest request = new ReportRequest();
request.setName("销售报表");
request.setType("monthly");
request.setStartDate(...);
request.setEndDate(...);
request.setIncludeDetails(true);
createReport(request);

这样改完,参数顺序再也不会错了。而且新增参数时,不需要改方法签名。

3. 用多态替换条件逻辑

遗留代码里最常见的“坏味道”就是大量的 if-else 或 switch。我见过一个订单处理系统,里面有一个 200 行的 switch 语句,处理 12 种订单类型。

安全的重构方式是:先提取每个 case 里的逻辑为独立方法,然后逐步引入策略模式。

// 第一步:提取方法
public void processOrder(Order order) {
    switch (order.getType()) {
        case "normal":
            processNormalOrder(order);
            break;
        case "vip":
            processVipOrder(order);
            break;
        // ... 其他类型
    }
}

// 第二步:引入策略接口
public interface OrderProcessor {
    void process(Order order);
}

public class NormalOrderProcessor implements OrderProcessor {
    public void process(Order order) {
        // 原来的 processNormalOrder 逻辑
    }
}

// 第三步:用 Map 替换 switch
Map<String, OrderProcessor> processors = new HashMap<>();
processors.put("normal", new NormalOrderProcessor());
processors.put("vip", new VipOrderProcessor());

public void processOrder(Order order) {
    OrderProcessor processor = processors.get(order.getType());
    if (processor != null) {
        processor.process(order);
    }
}

每一步都有测试保护,每一步都不改行为。这就是安全重构的精髓。

避坑指南:我曾经在重构一个遗留系统时,一次性改了 5 个文件,结果测试全红了。后来我学会了一个原则:一次只改一个东西。要么只改结构不改行为,要么只改行为不改结构。两者同时改,风险翻倍。

知识体系总览

下面这张图总结了本章的核心逻辑。你可以看到,整个重构过程是环环相扣的:

遗留系统重构核心流程 识别遗留代码 添加特性测试(输入输出验证) 安全重构手法 提取方法 引入参数对象 多态替换条件 核心原则:一次只改一个东西

总结

遗留系统重构,说白了就是三件事:理解它、保护它、改造它。理解靠的是读代码和画图,保护靠的是加测试,改造靠的是安全的手法。

我个人最深的体会是:不要试图一次性搞定所有事情。遗留系统的问题往往积累了几年甚至十几年,你不可能在一周内解决。每次改一点,每次都有测试保护,慢慢来,反而最快。

嗯,今天就聊到这里。下一节我们会继续深入,聊聊更复杂的遗留系统重构场景,比如如何拆解“上帝类”、如何处理全局状态等。到时候见。