4、依赖注入重构:将Classic中的硬编码依赖改为Adaptive的依赖注入模式
4.1 硬编码依赖——Classic时代的“死结”
说实话,我在Classic项目里见过最多的“坏味道”,就是硬编码依赖。
什么叫硬编码依赖?说白了,就是一个类里直接new出另一个类的实例。比如这样:
public class OrderService {
private EmailSender emailSender = new EmailSender();
private Logger logger = new FileLogger();
public void process(Order order) {
emailSender.send(order);
logger.log("订单已处理");
}
}
看着挺简单对吧?但问题来了——你想换一个日志方式怎么办?想测试时用Mock的EmailSender怎么办?
嗯,只能改代码。改完一个地方,还得改所有用到的地方。我接手过一个老项目,光改一个日志组件就改了37个文件。那滋味,谁改谁知道。
4.2 依赖注入的核心思想
Adaptive模式怎么解决这个问题的?一句话:别自己new,让别人给你送进来。
依赖注入(DI)的核心就三个字:控制反转。对象不再自己创建依赖,而是通过构造函数、属性或者方法参数传进来。
我个人习惯用构造函数注入,原因很简单——对象一创建,依赖就到位,不会出现空指针这种低级错误。
依赖注入三要素:
- 接口抽象——依赖的是接口,不是具体实现
- 外部注入——依赖由容器或调用方提供
- 生命周期管理——谁创建,谁销毁
4.3 从Classic到Adaptive:一步步重构
咱们拿刚才的OrderService开刀。先定义接口:
public interface IEmailSender {
void send(Order order);
}
public interface ILogger {
void log(String message);
}
然后让具体实现去实现这些接口:
public class SmtpEmailSender implements IEmailSender {
public void send(Order order) {
// 用SMTP发邮件
}
}
public class FileLogger implements ILogger {
public void log(String message) {
// 写文件日志
}
}
最后,OrderService不再自己new了,而是通过构造函数接收:
public class OrderService {
private final IEmailSender emailSender;
private final ILogger logger;
// 依赖从外面注入
public OrderService(IEmailSender emailSender, ILogger logger) {
this.emailSender = emailSender;
this.logger = logger;
}
public void process(Order order) {
emailSender.send(order);
logger.log("订单已处理");
}
}
你看,OrderService现在只关心业务逻辑,不关心谁发邮件、怎么发。这就是解耦。
4.4 容器登场——谁来做注入的“苦力”
有人会问:那谁去new这些依赖呢?总得有人干这活吧?
没错,这就是DI容器的职责。在Adaptive架构里,我们通常用轻量级容器来管理依赖。比如:
// 伪代码示意
Container container = new Container();
container.register(IEmailSender.class, SmtpEmailSender.class);
container.register(ILogger.class, FileLogger.class);
// 容器自动注入
OrderService service = container.resolve(OrderService.class);
我建议你在项目初期就引入容器。别等到依赖关系乱成一锅粥了再动手。我曾经在一个项目里,硬编码依赖和DI混着用,结果调试时根本分不清哪个对象是谁创建的。那叫一个头疼。
我的经验:选容器时别贪大。小项目用简单的工厂模式就够了,大项目再上Spring、Guice这类重量级框架。别为了用DI而用DI,工具是为人服务的。
4.5 避坑指南——我踩过的那些坑
重构依赖注入,有几个坑我得提醒你:
我曾经犯过的错:
- 循环依赖——A依赖B,B又依赖A。容器直接报错。解决办法:拆出中间接口,或者用属性注入打破循环。
- 过度抽象——每个类都搞个接口,结果接口比实现类还多。没必要。只有需要替换或扩展的依赖才抽象。
- 忘记释放资源——数据库连接、文件句柄这些,容器只管创建,不管销毁。你得自己实现Disposable模式。
4.6 测试性——DI带来的最大红利
硬编码依赖最大的痛是什么?不好测。
你想测试OrderService,结果它内部new了真正的EmailSender,每次跑测试都真的发邮件。这谁受得了?
用了DI之后,测试变得极其简单:
// 测试时注入Mock对象
IEmailSender mockSender = mock(IEmailSender.class);
ILogger mockLogger = mock(ILogger.class);
OrderService service = new OrderService(mockSender, mockLogger);
service.process(order);
// 验证行为
verify(mockSender).send(order);
verify(mockLogger).log("订单已处理");
你想想看,没有DI,这些Mock怎么塞进去?只能靠反射或者改代码。有了DI,测试代码写起来跟呼吸一样自然。
4.7 重构节奏——别想一口吃成胖子
最后说说重构的节奏。我见过有人想一天之内把整个项目的硬编码依赖全改完。结果呢?改到一半,项目跑不起来了,回退又舍不得,卡在那进退两难。
我的建议是:从底层往上改,从叶子节点开始。
| 步骤 | 做什么 | 注意什么 |
|---|---|---|
| 第一步 | 找出所有new出来的依赖 | 别漏了静态工厂方法 |
| 第二步 | 为可替换的依赖定义接口 | 只抽象那些可能变化的 |
| 第三步 | 逐个类改为构造函数注入 | 每次改完跑一遍测试 |
| 第四步 | 引入容器管理依赖 | 从最外层开始注册 |
| 第五步 | 清理旧的new代码 | 确保没有遗漏 |
嗯,这一步一步来,稳扎稳打。别急,重构不是百米冲刺,是马拉松。
依赖注入重构完了,你的代码会变得灵活、可测、可扩展。说白了,就是给项目装上了“可插拔”的能力。下一章咱们聊聊更高级的话题——如何用装饰器模式增强现有功能而不改代码。