什么是依赖注入(DI)控制反转IoC
一、从紧耦合问题说起假设我们有一个UserService需要发送邮件通常会直接new一个EmailService// ❌ 紧耦合示例 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 容器在实例化时把依赖“注入”进去 → 控制权反转给了容器。// ✅ 符合 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. 简单示例// email.service.ts import { Injectable } from nestjs/common; Injectable() // 标记为可注入的提供者 export class EmailService { send(message: string) { console.log(Sending: ${message}); } }// 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); } }// 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 启动时扫描AppModule的providers创建每个提供者的实例默认单例。创建UserService时发现它的构造函数需要EmailService于是从容器中取出已创建的EmailService实例注入进去。3. 注入令牌Injection Token// 使用自定义令牌 import { Inject } from nestjs/common; const EMAIL_SERVICE EMAIL_SERVICE; Injectable() class UserService { constructor(Inject(EMAIL_SERVICE) private emailService) {} }然后在模块中通过useClass、useValue、useFactory等方式提供Module({ providers: [ { provide: EMAIL_SERVICE, useClass: EmailService, }, ], }) export class AppModule {}4. 作用域ScopeNestJS 中的提供者默认是单例整个应用共享同一个实例。你也可以改为请求作用域或瞬态作用域Injectable({ scope: Scope.REQUEST }) // 每个请求新建实例 export class RequestScopedService {}五、为什么要在 NestJS 中使用 DI/IoC测试示例使用 Jestdescribe(UserService, () { let userService: UserService; let mockEmailService: PartialEmailService; 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); }); });