4Working Effectively with Legacy Code

分解依赖

解耦技术

依赖注入接口提取子类化方法提取

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第4章:分解依赖(Breaking Dependencies)

一、导读

1.1 本章学习目标

第4章讲解如何分解遗留代码中的依赖——让代码可以被测试。

本章的核心学习目标包括:

  • 理解依赖的类型
  • 掌握接口提取技术
  • 学会使用子类化和方法提取
  • 理解依赖注入在遗留代码中的应用

1.2 为什么本章如此重要

遗留代码难以测试的根本原因是依赖——对数据库、网络、文件系统、时间的依赖。分解这些依赖,才能将代码隔离到测试环境中。

1.3 核心问题引导

遗留代码中最常见的依赖有哪些?

如何在不修改原始代码的情况下分解依赖?

接口提取如何帮助测试?

什么时候应该使用子类化而不是接口提取?


二、核心概念详解

2.1 依赖的类型

1. 对象依赖:new 创建的具体对象
2. 全局依赖:全局变量、单例
3. 环境依赖:系统时间、环境变量
4. 外部依赖:数据库、网络、文件系统
5. 回调依赖:事件处理器、回调函数

2.2 接口提取

java
// 遗留代码
public class OrderService {
    public void placeOrder(Order order) {
        Database db = new Database();    // 硬编码依赖
        db.save(order);
        EmailService email = new EmailService();
        email.send(order.getCustomer(), "Order placed");
    }
}

// 提取接口
public interface Repository {
    void save(Order order);
}

public interface Notifier {
    void notify(Customer customer, String message);
}

// 重构后的代码
public class OrderService {
    private Repository repository;
    private Notifier notifier;
    
    public OrderService(Repository repo, Notifier notifier) {
        this.repository = repo;
        this.notifier = notifier;
    }
    
    // 保持向后兼容
    public OrderService() {
        this(new DatabaseRepository(), new EmailNotifier());
    }
    
    public void placeOrder(Order order) {
        repository.save(order);
        notifier.notify(order.getCustomer(), "Order placed");
    }
}

2.3 方法提取

java
// 遗留代码
public class Report {
    public String generate() {
        // ... 100 行代码
        String data = fetchFromDatabase();    // 难以测试
        // ... 更多代码
        return format(data);
    }
    
    private String fetchFromDatabase() {
        // 直接访问数据库
    }
}

// 提取方法,创建接缝
public class Report {
    public String generate() {
        String data = fetchData();    // 可覆盖的方法
        return format(data);
    }
    
    // 提取为可覆盖的方法
    protected String fetchData() {
        return fetchFromDatabase();
    }
}

// 测试子类
public class TestReport extends Report {
    private String testData;
    
    public void setTestData(String data) {
        this.testData = data;
    }
    
    @Override
    protected String fetchData() {
        return testData;
    }
}

三、实战案例

3.1 案例一:分解数据库依赖

java
// 遗留代码
public class UserService {
    public User findUser(String id) {
        Connection conn = DriverManager.getConnection(DB_URL);
        PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
        stmt.setString(1, id);
        ResultSet rs = stmt.executeQuery();
        // ... 映射结果
        return user;
    }
}

// 分解依赖
public interface UserFinder {
    User findById(String id);
}

public class UserService {
    private UserFinder finder;
    
    public UserService(UserFinder finder) {
        this.finder = finder;
    }
    
    public UserService() {
        this(new JdbcUserFinder());
    }
    
    public User findUser(String id) {
        return finder.findById(id);
    }
}

// 测试
@Test
void testFindUser() {
    UserFinder mockFinder = id -> new User(id, "Test User");
    UserService service = new UserService(mockFinder);
    User user = service.findUser("123");
    assertEquals("Test User", user.getName());
}

四、本章小结

依赖类型:对象、全局、环境、外部、回调。

接口提取:将具体实现替换为接口。

方法提取:将依赖调用提取为可覆盖的方法。

向后兼容:保留默认构造函数,不影响现有代码。


五、思考与练习

在一个遗留系统中找出5个需要分解的依赖。

接口提取和方法提取各适用于什么场景?

如何在不修改调用方代码的情况下分解依赖?


4/7分解依赖
0%