# 定义

依赖注入(Dependency Injection, DI)是一种设计模式,用来管理对象的依赖关系。核心思想是将对象所依赖的组件交由外部注入,而不是让对象自己创建或管理它们的依赖。这样做的好处是可以减少代码的耦合性,提高代码的可维护性、测试性和扩展性。

依赖注入其实 没有跳过或者使用其他方法代替【生成实例】这个过程,而是采用某个机制(如 IoC 容器)来管理依赖的实例化和分发,改变了传统的由代码显式创建实例的行为。这种机制核心是 ** 控制反转(Inversion of Control, IoC):** 组件不再主动创建依赖,而是交由容器来提供。更多内容可查看第五点:详解。

# 核心概念

  1. 依赖:一个类中依赖的其他类或组件,通常是服务、库等功能模块。
  2. 注入:把依赖关系从外部传入到当前类中,而不是在类内部直接创建依赖对象。

# 三种方式

  1. 构造函数注入:通过构造函数传递依赖对象。
  2. 方法注入:通过方法参数传递依赖对象。
  3. 属性注入:通过类的属性传递依赖对象。

# 优点

  • 解耦:类不再需要直接管理它的依赖对象,依赖的创建与管理交给外部系统来处理。
  • 可测试性:可以轻松地替换依赖为测试版本(例如 Mock 对象),使得单元测试更容易。
  • 可维护性和扩展性:当需要更换某个依赖实现时,无需修改依赖使用的代码。

# 简单示例

  1. 构造函数注入
class Engine {
  start() {
    console.log("Engine started");
  }
}
class Car {
  constructor(engine) {
    this.engine = engine;
  }
  startCar() {
    this.engine.start();
  }
}
// 外部创建依赖并注入
const engine = new Engine();
const car = new Car(engine);
car.startCar();
  1. 方法注入
class Car {
  startCar(engine) {
    engine.start();
  }
}
// 使用时将依赖通过方法传入
const engine = new Engine();
const car = new Car();
car.startCar(engine);
  1. 属性注入
class Car {
  setEngine(engine) {
    this.engine = engine;
  }
  startCar() {
    this.engine.start();
  }
}
// 使用时将依赖通过属性传入
const engine = new Engine();
const car = new Car();
car.setEngine(engine);
car.startCar();

# 详解

# 容器生成实例的方式

  • 单例:默认情况下,依赖通常以单例的形式提供。容器会在首次请求时创建实例,并缓存起来。之后的所有请求都返回相同的实例。

这种模式下,如果在不同文件中注入了相同依赖,一方更新,另一方也会导致更新

class Logger {}
Container.register(Logger); // 单例注册
  • 多例:如果需要每次都创建新的实例,可以通过工厂函数或特定的配置。
Container.register(Logger, {
  useFactory: () => new Logger(),
}); // 每次都会调用工厂函数生成实例

# 和直接 new 生成实例的区别

依赖注入的主要价值并不在于生成实例本身,而在于以下几点:

  1. 解耦

通过 DI,组件只依赖接口(契约)而非具体实现。例如:

class Service {
  constructor(private logger: ILogger) {}
}

组件 Service 并不知道 ILogger 的具体实现。容器可以动态地注入不同的实现(如 ConsoleLogger 或 FileLogger)。

  1. 可测试性

DI 容易让依赖的实例被替换,例如在测试环境中注入模拟对象(Mock)。

Container.register(Logger, {
  useValue: new MockLogger(),
});
  1. 生命周期管理

容器可以统一管理依赖的生命周期,比如创建、销毁等,避免手动管理实例的复杂性。

  1. 灵活性

通过容器可以灵活地改变依赖。例如,可以根据配置动态注入不同实现,而无需改动组件代码。