依赖注入的理解
依赖注入(Dependency Injection,简称 DI)是面向对象编程中控制反转(IoC)思想的具体实现,核心是 “让对象的依赖由外部容器来创建和注入,而非对象自己创建”,从而降低代码耦合度,提高灵活性和可测试性。
一、为什么需要依赖注入?
没有 DI 时,对象通常会自己创建依赖,导致代码 “高耦合”:
// 传统方式:A 自己创建 B 的实例,两者强耦合
public class A {
private B b;
public A() {
this.b = new B(); // A 直接依赖 B 的具体实现
}
}
这种写法的问题:
- 若 B 的构造方法变化(如新增参数),A 的代码也必须修改;
- 若想替换 B 为其子类(如
BSub),需修改 A 的源码; - 单元测试时,无法用 mock 对象替代 B,测试困难。
二、依赖注入的核心思想
依赖注入通过 **“外部容器”**(如 Spring IoC 容器)接管对象的依赖创建,对象只需 “声明依赖”,无需关心依赖如何创建。
// 依赖注入方式:A 只声明依赖 B,不负责创建
public class A {
private B b;
// 构造方法注入:容器通过构造方法传入 B 的实例
public A(B b) {
this.b = b; // 依赖由外部传入,A 与 B 的创建解耦
}
}
此时,B 的实例由容器创建并 “注入” 到 A 中,A 只依赖 B 的接口(或抽象类),不依赖具体实现。
三、依赖注入的常见实现方式
1. 构造方法注入(推荐)
通过对象的构造方法传入依赖,确保对象创建时就拥有所有必要的依赖(不可变依赖)。
public class UserService {
private UserDao userDao;
// 构造方法注入
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
2. Setter 方法注入
通过 setter 方法动态设置依赖,适合 “可选依赖”(可在对象创建后修改)。
public class UserService {
private UserDao userDao;
// Setter 注入
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
3. 字段注入(注解方式)
通过注解(如 Spring 的 @Autowired)直接在字段上标记依赖,由容器自动注入(代码简洁,但不利于测试)。
public class UserService {
// 字段注入
@Autowired
private UserDao userDao;
}
四、场景举例
假设我们有一个类 A,它需要依赖类 B 完成功能。在没有依赖注入的情况下,A 通常会自己创建 B 的实例:
// 父类 B
class B {
public void doSomething() {
System.out.println("B 执行操作");
}
}
// 子类 BSub(继承 B,扩展了功能)
class BSub extends B {
@Override
public void doSomething() {
System.out.println("BSub 执行增强操作");
}
}
// 类 A 依赖 B(传统方式)
class A {
private B b;
public A() {
// 硬编码:A 直接创建 B 的实例
this.b = new B();
}
public void work() {
b.doSomething(); // 使用 B 的功能
}
}
问题出现:需要替换为子类 BSub 时
如果后续需求变化,希望 A 改用 B 的子类 BSub(比如需要更强大的功能),由于 A 的构造方法中是硬编码 new B(),必须修改 A 的源码:
// 修改 A 的源码,将 new B() 改为 new BSub()
class A {
private B b;
public A() {
this.b = new BSub(); // 必须修改 A 的代码
}
// ...
}
为什么这是问题?
- 耦合过高:
A不仅依赖B的功能,还 “绑定” 了B的具体创建方式,两者形成强耦合。 - 扩展性差:每次更换依赖的实现(如子类、其他实现类),都需要修改
A的源码,违反了 “开闭原则”(对扩展开放,对修改关闭)。 - 维护成本高:如果有多个类像
A这样依赖B,更换为BSub时需要修改所有相关类的源码。
依赖注入如何解决?
使用依赖注入后,A 不再自己创建 B,而是由外部传入(注入):
class A {
private B b;
// 依赖注入:通过构造方法接收 B 的实例(可以是 B 或其任何子类)
public A(B b) {
this.b = b; // 不关心 b 是 B 还是 BSub,只要是 B 类型即可
}
public void work() {
b.doSomething();
}
}
此时,若想使用 BSub,只需在创建 A 时传入 BSub 的实例,无需修改 A 的源码:
// 使用 B 时
A a1 = new A(new B());
a1.work(); // 输出:B 执行操作
// 改用 BSub 时,只需修改创建 A 的地方
A a2 = new A(new BSub());
a2.work(); // 输出:BSub 执行增强操作
“若想替换 B 为其子类(如 BSub),需修改 A 的源码” 这句话的核心是:
传统硬编码方式中,对象的依赖关系写死在代码里,导致更换依赖实现时必须修改原对象的源码,造成高耦合和低扩展性。
而依赖注入通过 “外部传入依赖” 的方式,彻底解决了这个问题,让对象之间的依赖关系更灵活、易维护。
四、依赖注入的优势
-
解耦:对象不再依赖具体实现,只需依赖接口,符合 “依赖倒置原则”。
- 例如:
UserService依赖UserDao接口,可轻松替换为MySQLDao或MongoDao,无需修改UserService代码。
- 例如:
-
提高可测试性:单元测试时,可通过注入 mock 对象(如 Mockito 模拟的
UserDao)隔离测试目标。 -
集中管理依赖:所有依赖由容器统一创建和管理,便于配置(如切换数据库实现、修改参数)。
-
减少样板代码:无需手动
new依赖对象,简化开发。
五、依赖注入的本质
DI 的本质是 **“将对象的创建权从自身转移到外部容器”**,通过 “被动接收依赖” 替代 “主动创建依赖”。这种 “控制反转” 让对象更专注于自身业务逻辑,而不是依赖的管理,最终实现代码的高内聚、低耦合。
例如,Spring 框架的 IoC 容器就是 DI 的典型实现:开发者通过配置(XML / 注解)声明对象和依赖关系,容器负责创建对象、注入依赖,开发者直接使用即可。
更多推荐




所有评论(0)