依赖注入(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 的代码
    }
    
    // ...
}
为什么这是问题?
  1. 耦合过高A 不仅依赖 B 的功能,还 “绑定” 了 B 的具体创建方式,两者形成强耦合。
  2. 扩展性差:每次更换依赖的实现(如子类、其他实现类),都需要修改 A 的源码,违反了 “开闭原则”(对扩展开放,对修改关闭)。
  3. 维护成本高:如果有多个类像 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 的源码” 这句话的核心是:
传统硬编码方式中,对象的依赖关系写死在代码里,导致更换依赖实现时必须修改原对象的源码,造成高耦合和低扩展性

而依赖注入通过 “外部传入依赖” 的方式,彻底解决了这个问题,让对象之间的依赖关系更灵活、易维护。

四、依赖注入的优势

  1. 解耦:对象不再依赖具体实现,只需依赖接口,符合 “依赖倒置原则”。

    • 例如:UserService 依赖 UserDao 接口,可轻松替换为 MySQLDao 或 MongoDao,无需修改 UserService 代码。
  2. 提高可测试性:单元测试时,可通过注入 mock 对象(如 Mockito 模拟的 UserDao)隔离测试目标。

  3. 集中管理依赖:所有依赖由容器统一创建和管理,便于配置(如切换数据库实现、修改参数)。

  4. 减少样板代码:无需手动 new 依赖对象,简化开发。

五、依赖注入的本质

DI 的本质是 **“将对象的创建权从自身转移到外部容器”**,通过 “被动接收依赖” 替代 “主动创建依赖”。这种 “控制反转” 让对象更专注于自身业务逻辑,而不是依赖的管理,最终实现代码的高内聚、低耦合。

例如,Spring 框架的 IoC 容器就是 DI 的典型实现:开发者通过配置(XML / 注解)声明对象和依赖关系,容器负责创建对象、注入依赖,开发者直接使用即可。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐