Nestjs系列(一):IoC 与 DI 核心原理,Nest 依赖管理的基石

87 阅读6分钟

要理解 Nest.js 中的 控制反转(IoC)  和 依赖注入(DI) ,核心是先明确:IoC 是 “解耦” 的设计思想,DI 是落地这一思想的技术手段

IoC 是目标(解耦、让组件依赖抽象而非具体实现),DI 是实现 IoC 的关键技术;没有 DI,IoC 很难落地,DI 是 IoC 思想在 Nest 中的具体体现。Nest 正是通过 DI 机制落地了 IoC 思想,最终实现代码解耦、可维护性和可测试性的提升。

一、先搞懂:什么是控制反转(IoC)?

1. 定义

IoC(Inversion of Control,控制反转)的核心是:将 “依赖对象的创建、管理和组装” 的控制权,从业务代码本身转移到框架(如 Nest)手中

简单说:原本是 “你自己找依赖”,现在是 “框架给你送依赖”—— 控制权从 “使用者” 反转到了 “框架”。

2. 没有 IoC 时:代码是怎样的?

假设我们有一个 UserService(处理用户逻辑)和 AppController(接收请求),AppController需要依赖 UserService 才能工作:

// 没有 IoC:业务代码自己创建依赖
class UserService {
  getUser() { return "张三"; }
}

class AppController {
  // 自己 new 依赖对象,控制权在 AppController 手里
  private userService = new UserService(); 

  getUserName() {
    return this.userService.getUser();
  }
}

// 使用
const controller = new AppController();
console.log(controller.getUserName()); // 张三

问题在哪?

  • 耦合严重:AppController 必须知道 UserService 的创建细节(比如构造函数需要什么参数),如果 UserService 依赖其他对象(如 DbService),AppController 也要手动创建 DbService,牵一发而动全身。
  • 难以测试:无法替换 UserService 的模拟实现(比如测试时需要假数据)。
  • 可维护性差:依赖关系分散在代码中,修改一个依赖需要改所有使用它的地方。

3. 有 IoC 时:框架接管控制权

Nest 作为框架,会统一管理所有 “可被依赖的对象”(称为 Provider)。业务代码只需要 “声明自己需要什么”,框架就会自动把依赖对象 “送过来”,无需手动创建:

// 有 IoC:框架管理依赖,业务代码只声明需求
import { Injectable, Controller, Get } from '@nestjs/common';

// 告诉 Nest:这是一个可被注入的依赖(Provider)
@Injectable()
class UserService {
  getUser() { return "张三"; }
}

@Controller()
class AppController {
  // 声明依赖:告诉 Nest 我需要 UserService
  constructor(private userService: UserService) {} 

  @Get()
  getUserName() {
    return this.userService.getUser();
  }
}

这里的关键变化:

  • AppController 不再手动 new UserService(),而是通过构造函数 “声明依赖”。
  • 依赖的创建、管理(比如单例 / 多例)由 Nest 框架接管,AppController 只关心 “用依赖”,不关心 “怎么来”。

这就是 “控制反转”:依赖的控制权从业务代码(AppController)反转到了 Nest 框架

二、再理解:什么是依赖注入(DI)?

1. 定义

DI(Dependency Injection,依赖注入)是 实现 IoC 思想的具体技术:框架在运行时,将某个对象(依赖)的实例,通过 “注入” 的方式传递给需要它的对象(使用者)。

简单说:IoC 是 “要反转控制权” 的目标,DI 是 “怎么反转” 的具体操作 —— 把依赖 “注入” 到使用者手中。

2. Nest 中 DI 的核心实现细节

Nest 的 DI 机制依赖三个核心部分,缺一不可:

(1)标记为 Provider(可被注入的依赖)

通过 @Injectable() 装饰器告诉 Nest:“这个类是一个可被管理的依赖,你可以创建它的实例并注入给其他对象”。

(2)注册 Provider 到模块

Nest 是模块化架构,所有 Provider 必须注册到 @Module() 的 providers 数组中,框架才会扫描并管理它:

import { Module } from '@nestjs/common';

@Module({
  controllers: [AppController], // 注册控制器(使用者)
  providers: [UserService],     // 注册服务(依赖)
})
export class AppModule {}
(3)注入依赖(三种常见方式)

Nest 会通过 Injector(注入器)解析依赖关系,将实例注入到使用者手中:

  • 构造函数注入(推荐) :最常用,通过构造函数参数声明依赖,Nest 自动注入:
@Controller()
class AppController {
  // 私有只读属性,Nest 自动创建 UserService 实例并注入
  constructor(private readonly userService: UserService) {} 
}
  • 属性注入(不推荐) :通过 @Inject() 装饰器直接注入属性(跳过构造函数),但会降低代码可读性:
@Controller()
class AppController {
  @Inject(UserService)
  private readonly userService: UserService;
}
  • 工厂注入(复杂场景) :当依赖需要复杂初始化(如传入配置)时,用工厂函数创建实例:
@Module({
  providers: [
    {
      provide: UserService, // 依赖标识
      useFactory: (configService: ConfigService) => {
        // 工厂函数:接收其他依赖,返回 UserService 实例
        return new UserService(configService.get('user.config'));
      },
      inject: [ConfigService], // 工厂函数的依赖
    },
  ],
})

3. DI 的核心好处(也是 IoC 的目标)

  • 解耦:使用者(AppController)不依赖依赖的具体实现,只依赖抽象(如果用接口),修改依赖实现不影响使用者。
  • 可测试:测试时可以轻松替换依赖为模拟对象(如 jest.mock()),无需修改业务代码。
  • 可维护:依赖集中管理,修改依赖的创建逻辑(如从单例改为多例)只需改一处。
  • 可扩展:新增依赖时,只需标记 @Injectable() 并注册,使用者直接声明即可,无需全局修改。

三、IoC 和 DI 的核心联系

很多人会混淆两者,但本质是 “思想” 与 “实现” 的关系:

  1. IoC 是设计思想,DI 是技术手段:IoC 定义了 “控制权反转” 的目标(让框架接管依赖),DI 是实现这个目标的具体方法(通过注入器将依赖传递给使用者)。
  2. DI 是 IoC 的主流实现方式:IoC 思想的落地方式有很多(如 “服务定位器模式”),但 DI 是最优雅、最常用的一种(Nest、Spring、Angular 等框架都采用 DI)。
  3. 两者相辅相成,缺一不可
    • 没有 IoC 思想,DI 就失去了设计目标(只是单纯的 “传参”);
    • 没有 DI 技术,IoC 思想就无法落地(无法实现 “控制权反转”)。
  1. Nest 中的定位:Nest 的 IoC 容器 = DI 机制 + 模块系统 + 注入器。其中,DI 是核心引擎,负责依赖的注册、解析、创建和注入,最终实现 IoC 思想的核心目标 —— 解耦。

四、总结:一张表分清 IoC 和 DI

维度控制反转(IoC)依赖注入(DI)
本质设计思想、架构原则具体技术、实现手段
核心反转 “依赖的创建 / 管理” 控制权将依赖实例 “注入” 到使用者手中
关系目标(为什么这么做)手段(怎么做到)
作用指导框架设计,明确解耦目标落地解耦,实现可维护 / 可测试
Nest 中的体现框架接管 Provider 的生命周期@Injectable()+ 注入器 + 模块注册

最后:一个通俗比喻

  • 没有 IoC/DI:你想吃面条,需要自己买面粉、揉面、擀面、煮面(自己创建所有依赖)。
  • 有 IoC/DI:你去面馆(Nest 框架),只需要告诉服务员 “我要一碗面条”(声明依赖),服务员(注入器)会给你端来做好的面条(注入依赖)—— 你不用关心面条是怎么制作的(控制权反转),只管吃(使用依赖)。

这里:

  • IoC = 你不用自己做面条(反转 “做面条” 的控制权);
  • DI = 服务员把面条端到你面前(将面条 “注入” 给你)。