依赖注入和控制反转

你好,我是南一

什么是控制反转?

控制反转 是面向对象编程的一种设计思想,用于降低模块间的耦合度,以 NestJS 为例,只需在UserController构造函数中声明依赖userService的类型,框架层会创建userService依赖实例注入UserController中,依赖的创建和管理都由 NestJS 容器完成。

javascript 复制代码
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}
}

这样做有什么好处?

传统方式:既要声明,又要自己 new,在创建实例处产生强耦合。

javascript 复制代码
// 原来
export class UserService {
  constructor() {}
}

export class UserController {
  private userService: UserService;
  
  constructor() {
    // 这里产生了强耦合:UserController 硬编码了 UserService 的实例化逻辑
    this.userService = new UserService();
  }
}

假设有10个地方使用了UserService,就会有10个地方进行UserService实例化;此时UserService增加一个依赖参数,那就需要修改10个地方,把 db 参数传入

javascript 复制代码
// 后来增加了依赖
export class UserService {
  constructor(private readonly db: DatabaseService) {}
}

此时,控制反转的好处就体现出来了,没有实例化逻辑,只需要在UserService上声明好DatabaseService,依赖会自动注入,其他10个使用到UserService的地方都不需要改。

底层原理解析

NestJS采用依赖注入的方式实现控制反转思想,通过 Reflect.metadata 获取构造函数的参数类型,在 DI 容器中查找对应类型的实例,自动调用构造函数并传入该实例。

依赖注入:其实就是把依赖在类外部实例化之后通过参数传入。使用 TypeScript 可以进一步简化赋值操作,只需给构造函数参数添加访问修饰符 private ,就能声明和赋值一个私有变量

typescript 复制代码
class B {
  constructor() {}
}

// class A {
//   constructor(b) {
//     this.b = b; // JS 传统写法
//   }
// }

class A {
  constructor(private b: B) {} // TS 写法
}

const b = new B(); // 在外部实例化 B
const a = new A(b); // 依赖注入

下面我用一个极简的 Demo 来演示 NestJS 控制反转(IoC)和依赖注入(DI)的底层实现原理。这个 Demo 会模拟 NestJS 的核心机制:装饰器收集元数据 + 容器扫描注册 + 自动实例化注入。

1. 核心工具:Reflect.metadata 模拟

NestJS 底层依赖 reflect-metadata 库来存储类的元数据。我们先实现一个简易版本:

typescript 复制代码
// 模拟 Reflect.metadata 的简易实现
const metadataMap = new Map<string, any>();

function defineMetadata(target: any, key: string, value: any) {
  const targetKey = target.name || target.constructor.name;
  if (!metadataMap.has(targetKey)) {
    metadataMap.set(targetKey, {});
  }
  metadataMap.get(targetKey)[key] = value;
}

function getMetadata(target: any, key: string) {
  const targetKey = target.name || target.constructor.name;
  return metadataMap.get(targetKey)?.[key];
}

2. 实现 @Injectable 装饰器

@Injectable() 装饰器的作用是标记一个类可以被容器管理:

typescript 复制代码
function Injectable() {
  return function(target: any) {
    // 标记该类为可注入的
    defineMetadata(target, 'injectable', true);
    
    // 获取构造函数参数的类型信息(实际 NestJS 通过 TypeScript 编译时生成)
    // 这里我们手动模拟
    const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
    defineMetadata(target, 'paramTypes', paramTypes);
  };
}

3. 实现简易 DI 容器

这是整个机制的核心------IoC 容器:

typescript 复制代码
class Container {
  private instances = new Map<string, any>();
  private providers = new Map<string, any>();

  // 注册提供者
  addProvider(token: string, provider: any) {
    this.providers.set(token, provider);
  }

  // 获取实例(自动解析依赖)
  get<T>(token: string): T {
    // 如果已有实例,直接返回(单例模式)
    if (this.instances.has(token)) {
      return this.instances.get(token);
    }

    const provider = this.providers.get(token);
    if (!provider) {
      throw new Error(`Provider ${token} not found`);
    }

    // 获取构造函数的参数类型
    const paramTypes = getMetadata(provider, 'paramTypes') || [];
    
    // 递归解析所有依赖
    const dependencies = paramTypes.map((dep: any) => {
      const depName = dep.name;
      return this.get(depName);
    });

    // 创建实例并注入依赖
    const instance = new provider(...dependencies);
    
    // 缓存实例(单例)
    this.instances.set(token, instance);
    
    return instance;
  }
}

4. 实现 @Module 装饰器

模块装饰器用于组织相关的 Controller 和 Service:

typescript 复制代码
function Module(metadata: { controllers: any[]; providers: any[] }) {
  return function(target: any) {
    defineMetadata(target, 'module', true);
    defineMetadata(target, 'controllers', metadata.controllers);
    defineMetadata(target, 'providers', metadata.providers);
  };
}

5. 完整 Demo 示例

现在我们来实际使用这个简易框架:

typescript 复制代码
// ========== 业务代码 ==========

// 1. 定义服务(可注入)
@Injectable()
class UserService {
  getUsers() {
    return [
      { id: 1, name: '张三' },
      { id: 2, name: '李四' }
    ];
  }
}

// 2. 定义另一个服务(依赖 UserService)
@Injectable()
class UserController {
  constructor(private userService: UserService) {}
  
  findAll() {
    return this.userService.getUsers();
  }
}

// 3. 定义模块
@Module({
  controllers: [UserController],
  providers: [UserService]
})
class AppModule {}

// ========== 启动应用 ==========

// 4. 创建容器并注册所有提供者
const container = new Container();

// 同时注册 controllers 和 providers
const moduleMetadata = {
  controllers: getMetadata(AppModule, 'controllers') || [],
  providers: getMetadata(AppModule, 'providers') || []
};

// 将所有类注册到容器
[...moduleMetadata.controllers, ...moduleMetadata.providers].forEach((cls: any) => {
  container.addProvider(cls.name, cls);
});

// 5. 获取 Controller 实例(自动注入依赖)
const controller = container.get<UserController>('UserController');
console.log(controller.findAll());
// 输出: [ { id: 1, name: '张三' }, { id: 2, name: '李四' } ]

实际 NestJS 的实现远比这个 Demo 复杂,但核心思想完全一致:利用 TypeScript 的装饰器和反射能力,构建一个智能的依赖管理容器。

相关推荐
小村儿6 小时前
连载14-实战篇--一个半月,我一个人和 Claude Code 搭出一套数字人工程
前端·后端·ai编程
愚公移码6 小时前
蓝凌EKP18产品:流程虚拟机(PVM)
java·开发语言·前端
promiseThen6 小时前
5 分钟上手 Markdown:标题到表格、代码块与简历实战
前端
web66liang6 小时前
webpack4+vue2项目使用 sass-embedded 导致的 DockerfIle 构建失败的问题
前端
Strayer6 小时前
拓扑管网 3D 可视化大屏demo:科技感(地图 + 拓扑 + 3D 空间)
前端·three.js·数据可视化
葬送的代码人生6 小时前
从 Vue 到 React:Tailwind CSS 布局 + BFF 代理实战
前端·react.js·架构
摸鱼老王不带86 小时前
不用后端,纯前端读出"网站眼里你的真实出口 IP"——聊聊 Cloudflare 的 cdn-cgi/trace
前端
程序员黑豆7 小时前
鸿蒙开发入门:Row 和 Column 布局组件详解
前端·harmonyos
hoLzwEge7 小时前
前端环境变量配置指南:.env 文件详解
前端·前端框架