MD 状态:🌱 分类:软件设计与架构 更新:2026/5/29

依赖注入

[!abstract] 一句话定义 依赖注入(Dependency Injection, DI)是一种设计模式:类不自己创建依赖对象,而是由外部”注入”进来,从而实现松耦合和可测试性。

为什么需要它?

想象你写了一个 OrderService,它需要调用 MySQLDatabase 来存数据:

class OrderService {
    private MySQLDatabase db = new MySQLDatabase(); // 直接创建依赖
}

问题来了:

  • 想换成 PostgreSQL? → 改源码,重新编译
  • 想写单元测试? → 必须连真实数据库,测试慢且不稳定
  • 想复用这个类? → 把 MySQL 依赖也带走了,耦合太深

没有依赖注入,每个类都像”焊死的乐高”——拼在一起容易,拆开重組不可能。

核心直觉

类比:餐厅 vs 自己做饭

场景类比代码表现
没有 DI你想吃牛排,得自己养牛、种菜、磨刀new MySQLDatabase()
有 DI你去餐厅,服务员把牛排端上来构造函数接收 Database 接口

关键转变:你不需要知道”牛排怎么做的”,只需要声明”我要一份牛排”

它是怎么工作的?

三种注入方式

graph TD
    A[依赖注入 DI] --> B[构造函数注入]
    A --> C[Setter 方法注入]
    A --> D[接口注入]
    
    B --> B1[通过构造函数参数传入<br/>✅ 最推荐]
    C --> C1[通过 setter 方法设置<br/>⚠️ 可选依赖时使用]
    D --> D1[通过接口方法传入<br/>❌ 较少使用]
    
    style B1 fill:#ccffcc
    style C1 fill:#ffffcc
    style D1 fill:#ffcccc

核心流程

sequenceDiagram
    participant Main as 主程序/容器
    participant Service as OrderService
    participant DB as Database 接口
    participant MySQL as MySQL 实现
    
    Note over Main: 1. 创建具体依赖
    Main->>MySQL: new MySQLDatabase()
    
    Note over Main: 2. 注入到 Service
    Main->>Service: new OrderService(db)
    Service->>DB: 依赖的是接口,不是具体类
    
    Note over Service: 3. 使用依赖
    Service->>DB: db.save(order)
    DB->>MySQL: 实际执行

代码对比

❌ 没有依赖注入(紧耦合)

class OrderService {
    private MySQLDatabase db = new MySQLDatabase();  // 硬编码依赖
    
    public void saveOrder(Order order) {
        db.insert(order);  // 绑死 MySQL
    }
}

// 测试时无法替换,必须连真实数据库

✅ 有依赖注入(松耦合)

// 1. 定义接口
interface Database {
    void insert(Order order);
}

// 2. Service 依赖接口
class OrderService {
    private Database db;
    
    // 构造函数注入
    public OrderService(Database db) {
        this.db = db;
    }
    
    public void saveOrder(Order order) {
        db.insert(order);  // 不关心具体实现
    }
}

// 3. 组装时决定用哪个实现
Database db = new MySQLDatabase();  // 或 new PostgresDatabase()
OrderService service = new OrderService(db);

// 4. 测试时注入 Mock
Database mockDb = new MockDatabase();
OrderService testService = new OrderService(mockDb);  // 测试不依赖真实数据库

关键组件 / 核心要素

组件作用类比
依赖(Dependency)一个类需要使用的外部对象餐厅需要的食材
注入(Injection)将依赖从外部传递给类的过程服务员端菜上桌
IoC 容器自动管理依赖创建和注入的框架餐厅后厨自动化系统
接口(Interface)依赖的抽象定义,解耦具体实现菜单(不关心做法)

与相关概念的关系

[!info] vs 控制反转 IoC

  • IoC 是更广义的原则:“控制权从程序转移到框架”
  • DI 是 IoC 的一种具体实现方式
  • 关系:IoC 是思想,DI 是实现手段

[!note] 依赖于 面向接口编程

  • DI 的前提是”依赖抽象而非具体”
  • 没有接口/抽象类,DI 就无从谈起
  • 接口定义了”需要什么能力”,DI 负责”提供具体实现”

[!tip] 被 IoC容器 管理

  • Spring、Guice、Autofac 等框架提供自动 DI
  • 容器负责:创建对象 → 注入依赖 → 管理生命周期
  • 小项目可以手动 DI,大项目用容器更高效

[!tip] 是 SOLID原则 中 D 的体现

  • D = Dependency Inversion Principle(依赖倒置原则)
  • 高层模块不应依赖低层模块,都应依赖抽象
  • DI 是实现这一原则的关键技术

典型应用场景

  • 单元测试 — 注入 Mock 对象,隔离被测代码,测试更快更稳定
  • 多实现切换 — 同一个接口,开发用内存数据库,生产用 MySQL
  • 微服务架构 — 服务间通过接口通信,便于独立部署和替换
  • 插件系统 — 核心框架定义接口,插件通过 DI 注入实现

常见误解与陷阱

[!danger] ❌ 误以为:DI 必须用框架(如 Spring) ✅ 实际上:手动 DI 完全可行,小项目不需要框架。框架只是自动化了 DI 的组装过程

[!danger] ❌ 误以为:DI 让代码变复杂了 ✅ 实际上:DI 增加了”组装”的复杂度,但降低了”使用和测试”的复杂度。长期来看,收益远大于成本

[!danger] ❌ 误以为:所有依赖都要注入 ✅ 实际上:只注入”变化的依赖”。像 StringList 这种稳定类型,直接创建即可

[!danger] ❌ 误以为:DI = 依赖注入容器 ✅ 实际上:DI 是设计模式,容器是实现工具。可以用构造函数手动注入,不需要容器

延伸阅读

  • 想深入理解原理 → 阅读 Martin Fowler 的经典文章 Inversion of Control Containers and the Dependency Injection pattern
  • 想看工程实践 → 学习 Spring Framework 的 @Autowired@Inject 注解
  • 想了解前沿进展 → 关注编译期 DI(如 Dagger)和函数式 DI(如 Reader Monad)

关联笔记

前置知识面向接口编程 · SOLID原则 同族概念控制反转 IoC · 工厂模式 · 服务定位器 应用场景单元测试 · 微服务架构 · Spring框架