什么是依赖注入(DI)&控制反转IoC

一、从紧耦合问题说起

假设我们有一个 UserService 需要发送邮件,通常会直接 new 一个 EmailService

javascript 复制代码
// ❌ 紧耦合示例
class EmailService {
  send(recipient: string, content: string) { /* ... */ }
}

class UserService {
  private emailService = new EmailService(); // 直接创建依赖

  register(email: string) {
    this.emailService.send(email, 'Welcome');
  }
}

问题

  • 难以替换 EmailService 的实现(如换成 SmsService
  • 测试 UserService 时必须真实调用 EmailService,无法使用模拟对象。

  • EmailService 的构造函数发生变化,所有使用它的地方都要修改。

二、控制反转(IoC)

控制反转 :将对象的创建依赖查找 的控制权从类内部转移到外部容器(或框架)。

简单说:别找我要依赖,我会告诉你我需要什么,由别人给我。

传统(正向)控制

类主动创建或查找依赖 → 类控制依赖。

类只声明自己需要什么依赖(比如通过构造函数参数),由外部(IoC 容器)在实例化时把依赖"注入"进去 → 控制权反转给了容器。

javascript 复制代码
// ✅ 符合 IoC 原则:UserService 不关心 EmailService 如何创建
class UserService {
  constructor(private emailService: EmailService) {} // 只声明需要
  // ...
}

此时谁负责创建 EmailService 并传给 UserService?------ IoC 容器(NestJS 运行时)

三、依赖注入(DI)

依赖注入 是实现 IoC 的一种具体技术。它指:将组件所需的依赖(通常是其他服务或对象)通过构造函数、属性或方法参数的形式注入到组件中,而不是由组件自己创建。

NestJS 主要使用 构造函数注入(官方推荐),也支持属性注入(较少用)。

常见注入方式
  • 构造函数注入:依赖在实例化时通过构造函数传入。

  • 属性注入 :通过 @Inject() 装饰器直接注入到类属性。

  • 方法注入:通过方法参数注入(NestJS 不常用)。

四、NestJS 中的依赖注入实现

1. 基础三要素
  • 提供者(Provider) :可被注入的类,通常用 @Injectable() 装饰。

  • 消费者(Consumer):需要依赖的类,在构造函数中声明参数类型。

  • 模块(Module) :组织提供者,NestJS 根据模块的 providers 数组注册可注入的类。

2. 简单示例
TypeScript 复制代码
// email.service.ts
import { Injectable } from '@nestjs/common';

@Injectable() // 标记为可注入的提供者
export class EmailService {
  send(message: string) {
    console.log(`Sending: ${message}`);
  }
}
TypeScript 复制代码
// user.service.ts
import { Injectable } from '@nestjs/common';
import { EmailService } from './email.service';

@Injectable()
export class UserService {
  // 构造函数参数中声明依赖,NestJS 会自动注入 EmailService 的实例
  constructor(private readonly emailService: EmailService) {}

  register() {
    this.emailService.send('Welcome');
  }
}
TypeScript 复制代码
// app.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { EmailService } from './email.service';

@Module({
  providers: [UserService, EmailService], // 注册所有提供者
})
export class AppModule {}

当 NestJS 启动时:

  • 扫描 AppModuleproviders,创建每个提供者的实例(默认单例)。

  • 创建 UserService 时,发现它的构造函数需要 EmailService,于是从容器中取出已创建的 EmailService 实例,注入进去。

3. 注入令牌(Injection Token)
TypeScript 复制代码
// 使用自定义令牌
import { Inject } from '@nestjs/common';

const EMAIL_SERVICE = 'EMAIL_SERVICE';

@Injectable()
class UserService {
  constructor(@Inject(EMAIL_SERVICE) private emailService) {}
}

然后在模块中通过 useClassuseValueuseFactory 等方式提供:

TypeScript 复制代码
@Module({
  providers: [
    {
      provide: EMAIL_SERVICE,
      useClass: EmailService,
    },
  ],
})
export class AppModule {}
4. 作用域(Scope)

NestJS 中的提供者默认是单例(整个应用共享同一个实例)。你也可以改为请求作用域或瞬态作用域:

TypeScript 复制代码
@Injectable({ scope: Scope.REQUEST }) // 每个请求新建实例
export class RequestScopedService {}

五、为什么要在 NestJS 中使用 DI/IoC?

测试示例(使用 Jest)
TypeScript 复制代码
describe('UserService', () => {
  let userService: UserService;
  let mockEmailService: Partial<EmailService>;

  beforeEach(async () => {
    mockEmailService = { send: jest.fn() };
    const module = await Test.createTestingModule({
      providers: [
        UserService,
        { provide: EmailService, useValue: mockEmailService }, // 注入 mock
      ],
    }).compile();

    userService = module.get(UserService);
  });

  it('should send email on register', () => {
    userService.register();
    expect(mockEmailService.send).toHaveBeenCalledWith('Welcome');
  });
});
相关推荐
今夕资源网2 小时前
indextts API 阅读 API 重磅升级!低延迟 + 音色管理 + 缓存全拉满 支持开源阅读小说软件,其他软件应该也通用
java·后端·spring
是宇写的啊2 小时前
SpringIoc和Di
java·开发语言
沐雪轻挽萤2 小时前
3. C++17新特性-带初始化的 if 和 switch 语句
开发语言·c++
xianluohuanxiang2 小时前
2026年深度:高精度气象+新能源,从风速误差到收益偏差,行业赋能正在重构电站盈利模型
大数据·开发语言·人工智能·机器学习
lifallen2 小时前
Paimon 与 ForSt 场景选型分析
java·大数据·flink
我登哥MVP2 小时前
【Spring6笔记】 - 12 - 代理模式
java·spring boot·笔记·spring·代理模式·aop
froginwe113 小时前
SQL PRIMARY KEY(主键)
开发语言
2401_885885043 小时前
视频短信接口集成起来复杂吗?API接入说明
开发语言·php·音视频
潇洒畅想3 小时前
1.2 希腊字母速查表 + 公式阅读实战
java·人工智能·python·算法·rust·云计算