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代码 确保没有遗漏

嗯,这一步一步来,稳扎稳打。别急,重构不是百米冲刺,是马拉松。

依赖注入重构完了,你的代码会变得灵活、可测、可扩展。说白了,就是给项目装上了“可插拔”的能力。下一章咱们聊聊更高级的话题——如何用装饰器模式增强现有功能而不改代码。