依赖注入
[!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] ❌ 误以为:所有依赖都要注入 ✅ 实际上:只注入”变化的依赖”。像
String、List这种稳定类型,直接创建即可
[!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框架