{T}

IOC控制反转与DI依赖注入

一、核心概念概述

1.1 什么是 IOC 和 DI

概念说明

  • IOC(Inversion of Control,控制反转):一种设计模式/设计原则
  • DI(Dependency Injection,依赖注入):IOC 的具体实现方式

核心理解

code
IOC 与 DI 的关系:
│
├──  IOC(控制反转)
│   ├── 本质:设计模式 / 设计原则
│   ├── 目的:降低代码耦合度
│   └── 思想:借助第三方实现依赖解耦
│
├──  DI(依赖注入)
│   ├── 本质:IOC 的具体实现
│   ├── 方式:在类外创建依赖对象
│   └── 优势:灵活、可测试、可维护
│
└──  一句话总结
    └── IOC 是思想,DI 是实现

1.2 为什么需要 IOC

传统开发的问题

code
传统开发模式的痛点:
│
├──  强耦合
│   ├── 类与类之间紧密依赖
│   ├── 修改一个类影响多个类
│   └── 难以替换和扩展
│
├──  难以测试
│   ├── 依赖对象在类内部创建
│   ├── 无法模拟依赖对象
│   └── 单元测试困难
│
├──  难以维护
│   ├── 代码修改影响范围大
│   ├── 需要修改多处代码
│   └── 违反开闭原则
│
└──  解决方案
    └── IOC + DI 实现解耦

二、从代码示例理解 IOC 与 DI

2.1 问题场景:强耦合的代码

需求描述

  • 学生(Student)需要使用手机(Phone)玩游戏
  • 最初实现:学生类直接依赖具体的手机类(iPhone)

强耦合实现(问题代码)

typescript
// ========== 问题代码:强耦合 ==========

/**
 * iPhone 类
 */
class iPhone {
  /**
   * 打游戏
   * @param name 学生姓名
   */
  playGame(name: string): void {
    console.log(`${name} 正在使用 iPhone 打游戏`);
  }
}

/**
 * 学生类
 * 问题:与 iPhone 强耦合
 */
class Student {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  /**
   * 玩游戏
   * 问题:直接在类内部创建 iPhone 对象
   */
  play(): void {
    //  强耦合:直接依赖具体的 iPhone 类
    const phone = new iPhone();
    phone.playGame(this.name);
  }

  getName(): string {
    return this.name;
  }

  setName(name: string): void {
    this.name = name;
  }
}

// ========== 使用示例 ==========

const student = new Student('张三');
student.play();
// 输出:张三 正在使用 iPhone 打游戏

//  问题:
// 1. 如果学生想用 Android 手机怎么办?
// 2. 必须修改 Student 类的代码
// 3. 违反开闭原则(对修改开放)
// 4. 难以进行单元测试

问题分析

code
强耦合的问题:
│
├──  依赖具体实现
│   └── Student 依赖 iPhone,而不是抽象的 Phone
│
├──  难以扩展
│   └── 想要使用 Android 必须修改 Student 类
│
├──  难以测试
│   └── 无法模拟手机对象进行单元测试
│
└──  违反设计原则
    ├── 违反依赖倒置原则(DIP)
    ├── 违反开闭原则(OCP)
    └── 违反单一职责原则(SRP)

2.2 解决方案:依赖注入

第一步:定义抽象接口

typescript
// ========== 定义手机接口(抽象层)==========

/**
 * 手机接口
 * 抽象层:定义手机的通用行为
 */
interface Phone {
  /**
   * 打游戏
   * @param name 学生姓名
   */
  playGame(name: string): void;
}

/**
 * iPhone 实现 Phone 接口
 */
class iPhone implements Phone {
  playGame(name: string): void {
    console.log(`${name} 正在使用 iPhone 打游戏`);
  }

  // iPhone 特有方法
  callSiri(): void {
    console.log('Hey Siri!');
  }
}

/**
 * Android 实现 Phone 接口
 */
class Android implements Phone {
  playGame(name: string): void {
    console.log(`${name} 正在使用 Android 打游戏`);
  }

  // Android 特有方法
  useGoogleAssistant(): void {
    console.log('OK Google!');
  }
}

/**
 * 华为手机实现 Phone 接口
 */
class Huawei implements Phone {
  playGame(name: string): void {
    console.log(`${name} 正在使用华为手机打游戏`);
  }

  // 华为特有方法
  useXiaoyi(): void {
    console.log('小艺小艺!');
  }
}

第二步:实现依赖注入

typescript
// ========== 使用依赖注入的学生类 ==========

/**
 * 学生类(解耦版本)
 * 通过依赖注入,不再依赖具体的手机类
 */
class DIStudent {
  private name: string;
  private phone: Phone; //  依赖抽象接口,而非具体实现

  /**
   * 构造函数注入
   * @param name 学生姓名
   * @param phone 手机对象(从外部注入)
   */
  constructor(name: string, phone: Phone) {
    this.name = name;
    this.phone = phone; //  依赖从外部注入,而非内部创建
  }

  /**
   * 玩游戏
   * 不再关心具体是什么手机
   */
  play(): void {
    //  调用接口方法,具体实现由注入的对象决定
    this.phone.playGame(this.name);
  }

  getName(): string {
    return this.name;
  }

  setName(name: string): void {
    this.name = name;
  }

  //  可以动态更换手机
  setPhone(phone: Phone): void {
    this.phone = phone;
  }
}

// ========== 使用示例 ==========

// 场景 1:学生使用 Android 手机
const phone1 = new Android();
const student1 = new DIStudent('张三', phone1);
student1.play();
// 输出:张三 正在使用 Android 打游戏

// 场景 2:学生使用 iPhone
const phone2 = new iPhone();
const student2 = new DIStudent('李四', phone2);
student2.play();
// 输出:李四 正在使用 iPhone 打游戏

// 场景 3:学生使用华为手机
const phone3 = new Huawei();
const student3 = new DIStudent('王五', phone3);
student3.play();
// 输出:王五 正在使用华为手机打游戏

// 场景 4:动态更换手机
student1.setPhone(new iPhone());
student1.play();
// 输出:张三 正在使用 iPhone 打游戏

2.3 核心变化对比

维度强耦合版本依赖注入版本
依赖对象创建类内部 new iPhone()类外部创建,构造函数注入
依赖关系依赖具体类 iPhone依赖抽象接口 Phone
耦合度高耦合低耦合
可扩展性差(需修改类代码)好(只需创建新实现类)
可测试性差(无法模拟依赖)好(可注入模拟对象)
设计原则违反 DIP、OCP符合 DIP、OCP、SRP

2.4 控制反转的本质

反转了什么?

code
控制反转的本质:
│
├──  反转前(强耦合)
│   ├── Student 类控制 iPhone 的创建
│   ├── Student 决定使用哪个手机
│   └── 控制权在 Student 类内部
│
├──  反转后(依赖注入)
│   ├── 外部容器控制手机的创建
│   ├── 外部决定注入哪个手机
│   └── 控制权转移到外部容器
│
└──  本质
    └── 控制权从类内部转移到外部容器

图解控制反转

code
传统方式:
Student ──────强依赖──────> iPhone
   │                              │
   └── 内部创建 new iPhone() ─────┘
   控制权在 Student


依赖注入方式:
                    ┌─────────────┐
                    │  外部容器    │
                    │  (Container)│
                    └──────┬──────┘
                           │ 控制权在外部
                           │
              ┌────────────┼────────────┐
              │            │            │
              ▼            ▼            ▼
          ┌───────┐   ┌─────────┐   ┌─────────┐
          │iPhone │   │ Android │   │ Huawei  │
          └───────┘   └─────────┘   └─────────┘
              │            │            │
              └────────────┼────────────┘
                           │ 注入
                           ▼
                      ┌─────────┐
                      │ Student │
                      └─────────┘

三、DI 的三种注入方式

3.1 构造函数注入

typescript
/**
 * 构造函数注入(最常用)
 * 优点:依赖关系明确,对象创建时就必须提供所有依赖
 */
class Student {
  private name: string;
  private phone: Phone;

  constructor(name: string, phone: Phone) {
    this.name = name;
    this.phone = phone;
  }

  play(): void {
    this.phone.playGame(this.name);
  }
}

// 使用
const student = new Student('张三', new iPhone());

3.2 属性注入

typescript
/**
 * 属性注入
 * 优点:灵活,可选依赖
 * 缺点:依赖关系不明确,可能为 null
 */
class Student {
  private name: string;
  public phone!: Phone; // 可选依赖

  constructor(name: string) {
    this.name = name;
  }

  play(): void {
    if (this.phone) {
      this.phone.playGame(this.name);
    } else {
      console.log('没有手机可玩');
    }
  }
}

// 使用
const student = new Student('张三');
student.phone = new iPhone(); // 后续注入
student.play();

3.3 方法注入

typescript
/**
 * 方法注入
 * 优点:最灵活,可以动态更换依赖
 * 缺点:依赖关系最不明确
 */
class Student {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  play(phone: Phone): void {
    phone.playGame(this.name);
  }
}

// 使用
const student = new Student('张三');
student.play(new iPhone());    // 用 iPhone 玩
student.play(new Android());   // 用 Android 玩

3.4 三种方式对比

注入方式优点缺点适用场景
构造函数注入依赖明确、不可变、易测试参数多时构造函数复杂必需依赖
属性注入灵活、可选依赖不明确、可能为 null可选依赖
方法注入最灵活、动态更换依赖最不明确临时依赖

四、NestJS 中的依赖注入

4.1 NestJS DI 核心概念

三大核心概念

code
NestJS 依赖注入三大核心:
│
├──  Provider(提供者)
│   ├── 被 @Injectable() 装饰的类
│   ├── 可以被注入到其他类中
│   └── Service、Repository、Helper 等
│
├──  Token(令牌)
│   ├── 用于标识 Provider
│   ├── 通常是类名或字符串
│   └── 用于在容器中查找 Provider
│
└──  Container(容器)
    ├── 管理所有 Provider 的创建和生命周期
    ├── 自动解析依赖关系
    └── 自动注入依赖

4.2 基本使用示例

完整示例:用户管理模块

typescript
// ========== 1. 定义接口(可选)==========

// src/users/interfaces/user.interface.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

// ========== 2. 创建 Service(Provider)==========

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { User } from './interfaces/user.interface';

@Injectable() //  标记为可注入的 Provider
export class UsersService {
  private users: User[] = [
    { id: 1, name: '张三', email: 'zhangsan@example.com' },
    { id: 2, name: '李四', email: 'lisi@example.com' },
  ];

  findAll(): User[] {
    return this.users;
  }

  findOne(id: number): User | undefined {
    return this.users.find(user => user.id === id);
  }

  create(user: User): User {
    const newUser = { ...user, id: Date.now() };
    this.users.push(newUser);
    return newUser;
  }
}

// ========== 3. 创建 Controller(注入 Service)==========

// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './interfaces/user.interface';

@Controller('users')
export class UsersController {
  //  构造函数注入:通过依赖注入获取 UsersService 实例
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll(): User[] {
    //  使用注入的 Service
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string): User | undefined {
    return this.usersService.findOne(+id);
  }

  @Post()
  create(@Body() user: User): User {
    return this.usersService.create(user);
  }
}

// ========== 4. 创建 Module(注册 Provider)==========

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],  // 注册控制器
  providers: [UsersService],        // 注册提供者(Service)
  exports: [UsersService],          // 导出 Service(供其他模块使用)
})
export class UsersModule {}

// ========== 5. 在根模块中导入 ==========

// src/app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],
})
export class AppModule {}

4.3 工作原理详解

NestJS DI 工作流程

code
NestJS 依赖注入工作流程:
│
├── 1⃣ 启动阶段
│   ├── 扫描所有 Module
│   ├── 注册所有 Provider 到容器
│   └── 分析依赖关系图
│
├── 2⃣ 实例化阶段
│   ├── 根据依赖关系图
│   ├── 按拓扑排序创建实例
│   └── 自动注入依赖
│
├── 3⃣ 运行阶段
│   ├── 接收 HTTP 请求
│   ├── 路由到对应 Controller
│   └── Controller 调用注入的 Service
│
└── 4⃣ 响应阶段
    └── 返回处理结果

依赖关系图示例

code
依赖关系图:

AppModule
│
├── UsersModule
│   │
│   ├── UsersController
│   │   └── depends on: UsersService
│   │
│   ├── UsersService
│   │   └── depends on: (none)
│   │
│   └── exports: UsersService
│
└── AuthModule
    │
    ├── AuthController
    │   └── depends on: AuthService
    │
    ├── AuthService
    │   └── depends on: UsersService (imported from UsersModule)
    │
    └── imports: UsersModule

4.4 高级用法:自定义 Provider

场景:注入第三方库或配置

typescript
// ========== 方式一:使用 useValue ==========

// src/database/database.module.ts
import { Module } from '@nestjs/common';

const databaseConfig = {
  host: 'localhost',
  port: 3306,
  username: 'root',
  password: 'password',
  database: 'test',
};

@Module({
  providers: [
    {
      provide: 'DATABASE_CONFIG', // 自定义 Token
      useValue: databaseConfig,   // 直接提供值
    },
  ],
  exports: ['DATABASE_CONFIG'],
})
export class DatabaseModule {}

// 使用
import { Inject, Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(
    @Inject('DATABASE_CONFIG') private readonly dbConfig: any,
  ) {
    console.log('数据库配置:', dbConfig);
  }
}

// ========== 方式二:使用 useFactory ==========

// src/database/database.module.ts
import { Module } from '@nestjs/common';

@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: (config: any) => {
        // 工厂函数:动态创建连接
        return createConnection(config);
      },
      inject: ['DATABASE_CONFIG'], // 注入依赖
    },
  ],
  exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}

// ========== 方式三:使用 useClass ==========

@Module({
  providers: [
    {
      provide: 'PhoneInterface',
      useClass: iPhone, // 可以在运行时替换为 Android
    },
  ],
})
export class PhoneModule {}

// 使用
@Injectable()
export class Student {
  constructor(
    @Inject('PhoneInterface') private phone: Phone,
  ) {}

  play(): void {
    this.phone.playGame('张三');
  }
}

4.5 循环依赖问题

问题场景

typescript
//  循环依赖示例

// service-a.service.ts
@Injectable()
export class ServiceA {
  constructor(private serviceB: ServiceB) {} // 依赖 ServiceB
}

// service-b.service.ts
@Injectable()
export class ServiceB {
  constructor(private serviceA: ServiceA) {} // 依赖 ServiceA
}

// 问题:A 依赖 B,B 依赖 A,形成循环依赖

解决方案:使用 forwardRef

typescript
//  解决循环依赖

// service-a.service.ts
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { ServiceB } from './service-b.service';

@Injectable()
export class ServiceA {
  constructor(
    @Inject(forwardRef(() => ServiceB))
    private serviceB: ServiceB,
  ) {}
}

// service-b.service.ts
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { ServiceA } from './service-a.service';

@Injectable()
export class ServiceB {
  constructor(
    @Inject(forwardRef(() => ServiceA))
    private serviceA: ServiceA,
  ) {}
}

// 或者在 Module 中使用
@Module({
  providers: [
    {
      provide: ServiceA,
      useClass: ServiceA,
    },
    {
      provide: ServiceB,
      useClass: ServiceB,
    },
  ],
})
export class AppModule {}

五、TypeScript 装饰器实现 DI 原理

5.1 reflect-metadata 简介

概念说明

reflect-metadata 是一个库,用于在运行时添加和读取元数据。NestJS 使用它来实现依赖注入。

安装

bash
npm install reflect-metadata

基本使用

typescript
// 在项目入口文件第一行导入
import 'reflect-metadata';

// 定义元数据
@Reflect.metadata('class', 'UserService')
class UserService {
  @Reflect.metadata('method', 'findAll')
  findAll() {
    return 'all users';
  }
}

// 读取元数据
const classMetadata = Reflect.getMetadata('class', UserService);
console.log(classMetadata); // 'UserService'

const methodMetadata = Reflect.getMetadata('method', UserService.prototype, 'findAll');
console.log(methodMetadata); // 'findAll'

5.2 手写简化版 DI 容器

完整实现

typescript
// ========== 简化版 DI 容器实现 ==========

import 'reflect-metadata';

// ========== 1. 定义装饰器 ==========

/**
 * Injectable 装饰器
 * 标记类为可注入的 Provider
 */
function Injectable() {
  return function(target: any) {
    // 在类上添加元数据,标记为可注入
    Reflect.defineMetadata('injectable', true, target);
  };
}

/**
 * Inject 装饰器
 * 用于属性注入
 */
function Inject(token?: string) {
  return function(target: any, propertyKey: string) {
    // 获取属性类型
    const type = Reflect.getMetadata('design:type', target, propertyKey);
    const injectionToken = token || type;
    
    // 在属性上添加元数据
    Reflect.defineMetadata('inject', injectionToken, target, propertyKey);
  };
}

// ========== 2. 定义容器类 ==========

/**
 * DI 容器
 */
class Container {
  private providers = new Map<any, any>();

  /**
   * 注册 Provider
   */
  register(token: any, provider: any) {
    this.providers.set(token, provider);
  }

  /**
   * 获取 Provider 实例
   */
  get<T>(token: any): T {
    // 检查是否已注册
    if (!this.providers.has(token)) {
      throw new Error(`Provider ${token} not found`);
    }

    // 如果是类,创建实例
    const Provider = this.providers.get(token);
    
    // 检查是否标记为 Injectable
    if (!Reflect.getMetadata('injectable', Provider)) {
      throw new Error(`${Provider.name} is not injectable`);
    }

    // 获取构造函数参数类型
    const paramTypes = Reflect.getMetadata('design:paramtypes', Provider) || [];
    
    // 递归解析依赖
    const dependencies = paramTypes.map((paramType: any) => this.get(paramType));
    
    // 创建实例
    const instance = new Provider(...dependencies);
    
    // 处理属性注入
    for (const key of Object.keys(instance)) {
      const injectionToken = Reflect.getMetadata('inject', instance, key);
      if (injectionToken) {
        instance[key] = this.get(injectionToken);
      }
    }
    
    return instance;
  }
}

// ========== 3. 使用示例 ==========

// 定义接口
interface Phone {
  playGame(name: string): void;
}

// 实现
@Injectable()
class iPhone implements Phone {
  playGame(name: string): void {
    console.log(`${name} 正在使用 iPhone 打游戏`);
  }
}

@Injectable()
class Android implements Phone {
  playGame(name: string): void {
    console.log(`${name} 正在使用 Android 打游戏`);
  }
}

@Injectable()
class Student {
  constructor(private phone: Phone) {}

  play(name: string): void {
    this.phone.playGame(name);
  }
}

// ========== 4. 注册和获取实例 ==========

const container = new Container();

// 注册 Provider
container.register('Phone', iPhone);
container.register(Student, Student);

// 获取实例
const student = container.get<Student>(Student);
student.play('张三');
// 输出:张三 正在使用 iPhone 打游戏

// 替换实现
container.register('Phone', Android);
const student2 = container.get<Student>(Student);
student2.play('李四');
// 输出:李四 正在使用 Android 打游戏

5.3 装饰器执行顺序

typescript
// 装饰器执行顺序示例

function ClassDecorator() {
  console.log('1. 类装饰器');
  return function(target: any) {
    console.log('4. 类装饰器执行');
  };
}

function PropertyDecorator() {
  console.log('2. 属性装饰器');
  return function(target: any, propertyKey: string) {
    console.log('5. 属性装饰器执行');
  };
}

function MethodDecorator() {
  console.log('3. 方法装饰器');
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('6. 方法装饰器执行');
  };
}

@ClassDecorator()
class Example {
  @PropertyDecorator()
  name: string = '';

  @MethodDecorator()
  method() {}
}

// 执行顺序:
// 1. 类装饰器(定义时)
// 2. 属性装饰器(定义时)
// 3. 方法装饰器(定义时)
// 4. 类装饰器执行(实例化时)
// 5. 属性装饰器执行(实例化时)
// 6. 方法装饰器执行(实例化时)

六、DI 在测试中的应用

6.1 单元测试优势

为什么 DI 有利于测试?

typescript
// ========== 不使用 DI(难以测试)==========

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

  async getUsers() {
    return this.httpService.get('/users');
  }
}

// 测试困难:无法模拟 HttpService


// ========== 使用 DI(易于测试)==========

class UserService {
  constructor(private httpService: HttpService) {} //  注入依赖

  async getUsers() {
    return this.httpService.get('/users');
  }
}

// 测试:可以注入模拟对象
const mockHttpService = {
  get: jest.fn().mockResolvedValue([{ id: 1, name: '张三' }]),
};

const userService = new UserService(mockHttpService as any);

6.2 完整测试示例

typescript
// ========== 被测试的 Service ==========

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { User } from './interfaces/user.interface';

@Injectable()
export class UsersService {
  private users: User[] = [];

  constructor(
    private logger: LoggerService,
    private validator: ValidatorService,
  ) {}

  findAll(): User[] {
    this.logger.log('获取所有用户');
    return this.users;
  }

  create(user: User): User {
    if (!this.validator.validate(user)) {
      throw new Error('用户数据验证失败');
    }
    
    const newUser = { ...user, id: Date.now() };
    this.users.push(newUser);
    this.logger.log(`创建用户:${newUser.name}`);
    
    return newUser;
  }
}

// ========== 测试文件 ==========

// src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { LoggerService } from '../common/logger.service';
import { ValidatorService } from '../common/validator.service';

describe('UsersService', () => {
  let service: UsersService;
  let loggerService: LoggerService;
  let validatorService: ValidatorService;

  // 模拟依赖
  const mockLoggerService = {
    log: jest.fn(),
    error: jest.fn(),
  };

  const mockValidatorService = {
    validate: jest.fn().mockReturnValue(true),
  };

  beforeEach(async () => {
    // 创建测试模块
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: LoggerService,
          useValue: mockLoggerService, // 注入模拟对象
        },
        {
          provide: ValidatorService,
          useValue: mockValidatorService, // 注入模拟对象
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    loggerService = module.get<LoggerService>(LoggerService);
    validatorService = module.get<ValidatorService>(ValidatorService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('findAll', () => {
    it('应该返回用户列表', () => {
      const result = service.findAll();
      expect(result).toEqual([]);
      expect(mockLoggerService.log).toHaveBeenCalledWith('获取所有用户');
    });
  });

  describe('create', () => {
    it('应该成功创建用户', () => {
      const user = { name: '张三', email: 'zhangsan@example.com' };
      const result = service.create(user as any);
      
      expect(result.name).toBe('张三');
      expect(mockValidatorService.validate).toHaveBeenCalled();
      expect(mockLoggerService.log).toHaveBeenCalledWith('创建用户:张三');
    });

    it('验证失败时应该抛出错误', () => {
      mockValidatorService.validate.mockReturnValue(false);
      
      const user = { name: '', email: 'invalid' };
      
      expect(() => service.create(user as any)).toThrow('用户数据验证失败');
    });
  });
});

七、最佳实践

7.1 设计原则

code
DI 最佳实践原则:
│
├──  依赖倒置原则(DIP)
│   ├── 高层模块不应依赖低层模块
│   ├── 两者都应依赖抽象
│   └── 抽象不应依赖细节
│
├──  单一职责原则(SRP)
│   ├── 每个 Service 只负责一个业务领域
│   ├── 避免上帝类(God Class)
│   └── 合理拆分职责
│
├──  接口隔离原则(ISP)
│   ├── 使用小而专一的接口
│   ├── 避免"胖"接口
│   └── 客户端不应依赖它不需要的方法
│
└──  开闭原则(OCP)
    ├── 对扩展开放
    ├── 对修改关闭
    └── 通过接口和 DI 实现扩展

7.2 命名约定

typescript
// ========== 推荐命名约定 ==========

// Service: 业务逻辑
@Injectable()
export class UsersService {}

// Repository: 数据访问
@Injectable()
export class UsersRepository {}

// Controller: 路由处理
@Controller('users')
export class UsersController {}

// Guard: 权限守卫
@Injectable()
export class AuthGuard implements CanActivate {}

// Interceptor: 拦截器
@Injectable()
export class LoggingInterceptor implements NestInterceptor {}

// Pipe: 管道
@Injectable()
export class ValidationPipe implements PipeTransform {}

// Filter: 过滤器
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {}

7.3 Module 组织最佳实践

typescript
// ========== 功能模块组织 ==========

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';

@Module({
  controllers: [UsersController],  // 控制器
  providers: [UsersService, UsersRepository],  // 提供者
  exports: [UsersService],  // 导出公共 API
})
export class UsersModule {}

// src/app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    UsersModule,
    AuthModule,
  ],
})
export class AppModule {}

// ========== 共享模块 ==========

// src/common/common.module.ts
import { Module, Global } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { ConfigService } from './config.service';

@Global() // 全局模块
@Module({
  providers: [LoggerService, ConfigService],
  exports: [LoggerService, ConfigService], // 导出后所有模块可用
})
export class CommonModule {}

八、常见问题与解决方案

问题原因解决方案
Nest can't resolve dependencies未在 Module 中注册 Provider@Moduleproviders 中添加 Service
循环依赖错误A 依赖 B,B 依赖 A使用 forwardRef 或重构代码
Provider 重复注册多个 Module 重复注册使用共享模块或在根模块统一注册
注入 undefinedProvider 未正确导出检查 Module 的 exports 配置
测试中依赖注入失败未在测试模块中注册 Provider使用 Test.createTestingModule 创建测试模块
作用域问题使用了错误的 Provider 作用域理解 Singleton、Request、Transient 作用域
私有 Provider 无法注入未在 exports 中导出在 Module 的 exports 中添加 Provider
装饰器顺序错误装饰器执行顺序问题理解装饰器执行顺序,正确组合使用

九、学习要点总结

核心要点

  1. IOC 本质:控制反转,将依赖对象的创建权从类内部转移到外部容器
  2. DI 本质:依赖注入,是 IOC 的具体实现方式
  3. 核心优势:降低耦合度、提高可测试性、提高可维护性
  4. NestJS 实现:通过 @Injectable() 装饰器和 Module 系统实现 DI
  5. 设计原则:依赖倒置原则(DIP)是 DI 的理论基础

行动建议

code
学习路径:
│
├──  第一阶段:理解概念(1-2 天)
│   ├── 理解 IOC 和 DI 的本质
│   ├── 对比强耦合和解耦的差异
│   └── 理解三种注入方式
│
├──  第二阶段:实践练习(1 周)
│   ├── 手写简化版 DI 容器
│   ├── 在 NestJS 中使用 DI
│   └── 编写单元测试
│
└──  第三阶段:深入应用(持续)
    ├── 理解 NestJS DI 内部实现
    ├── 解决循环依赖问题
    └── 设计合理的模块架构

十、延伸学习资源

官方资源

推荐阅读

  • 《设计模式:可复用面向对象软件的基础》- 依赖倒置原则
  • 《Clean Architecture》- 依赖注入与架构设计
  • Martin Fowler - Inversion of Control Containers and the Dependency Injection pattern

练习建议

  1. 练习 1:手写一个简化版的 DI 容器
  2. 练习 2:将现有项目中的强耦合代码重构为依赖注入
  3. 练习 3:编写单元测试,使用模拟对象验证 DI 的优势
  4. 练习 4:解决一个循环依赖问题
  5. 练习 5:设计一个模块化架构,合理使用共享模块

延伸思考

code
思考题:
│
├──  IOC 和工厂模式的区别是什么?
├──  为什么 NestJS 使用 Singleton 作为默认作用域?
├──  如何避免过度使用 DI?
├──  DI 在微服务架构中的应用?
└──  如何设计一个高性能的 DI 容器?

附录:DI 核心概念速查表

概念说明NestJS 实现
Provider可注入的服务@Injectable()
Token标识 Provider类名、字符串、Symbol
Container管理实例的容器NestJS 内部实现
Injection注入方式构造函数、属性、方法
Module组织 Provider@Module()
Scope实例作用域Singleton、Request、Transient
Circular Dependency循环依赖forwardRef

笔记整理完成时间:2026-03-07
下一章节预告:NestJS 模块化架构深入