第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个需要分解的依赖。
接口提取和方法提取各适用于什么场景?
如何在不修改调用方代码的情况下分解依赖?