IoC 解决了什么问题?

IoC 使开发者免去手动创建和组装对象的麻烦,改成声明依赖,让工具自动创建组装对象

Posted by chanweiyan on April 25, 2026

nestjs 中有很多对象

  • Controller 对象,接收 http 请求,调用 service,返回响应
  • Service 对象,实现业务逻辑
  • Repository 对象,实现对数据库的增删改查
  • 数据库连接对象 DataSource
  • 配置对象 Config

对象间关系复杂

  • Controller 依赖 Service 实现业务逻辑
  • Service 依赖 Repository 来做增删改查
  • Repository 依赖 DataSource 来建立连接
  • DataSource 依赖 Config 获取用户名和密码等信息

导致创建这些对象很复杂

  • 问题1:需要理清它们之间的关系,哪个先创建,哪个后创建:
1
2
3
4
5
6
7
8
9
10
// @ts-nocheck
const config = new Config({ username: "foo", password: "123456" });

const dataSource = new DataSource(config);

const repository = new Repository(dataSource);

const service = new Service(repository);

const controller = new Controller(service);
  • 问题2: 要经过一系列初始化后,才能使用 Controller 对象
  • 问题3: config,dataSource, repository, service, controller 等这些对象不需要每次都new一个新的,一直用一个就行,也就是保持单例。

解决方式 - 反转控制 IoC(Inverse of Control)

之前手动创建和组装对象很麻烦,改成在 class 上声明依赖了什么,然后,让工具分析声明的依赖关系, 根据先后顺序自动把对象创建好并组装起来。

声明依赖关系

IoC 有一个放对象的容器,程序初始化的时候,会扫描 class 上声明的依赖关系, 然后,把这些 class 都 new 一个实例放到容器里。 创建对象的时候,还会把它们依赖的对象注入进去。

这种依赖注入的方式叫做 DI(Dependency Injection)。

为什么叫 IoC?

  • 以前是手动 new 依赖对象,然后,组装起来。
  • 现在是声明依赖了什么,等待注入。
  • 从主动创建依赖到被动等待依赖注入,这就是反转控制 IoC

AppService 声明了 @Injectable,代表这个class 可注入,nest 会把它的对象放到 IoC 容器里

@Injectable

AppController 声明了 @Controller,代表这个 class 可以被注入,nestjs 会把它放到 IoC 容器里

@Controller

AppController 构造器参数依赖了 AppService

为什么 Controller 是单独的装饰器呢?

  • 因为 Service 是可以被注入的,也可以注入到别的对象,所以用 @Injectable 声明。
  • 而 Controller 只需要被注入,所以,nest 大度给它加了 @Controller 的装饰器。

通过 @Module 声明模块,其中 controllers 是控制器,只能被注入。providers 中的可以被注入,也可以注入别的对象。

@Module

然后,在入口模块里跑起来:

main.ts

那么 nest 就会从 AppModule 开始解析 class 上通过装饰器声明的依赖信息,自动创建和组装对象。

AppModule依赖关系

nest 模块机制

把不同业务的 controller,service等放到不同的模块里。

nest g module other 生成一个模块,会在 AppModule 里自动 imports 这个模块,当 import 别的模块后,那个模块 exports 的 provider就可以在当前模块注入了。

exports-imports

exports

DI

1
2
3
4
5
6
7
8
9
10
11
12
13
// @ts-nocheck
import { OtherService } from "./other/other.service";
import { Inject, Injectable } from "@nestjs/common";

@Injectable()
export class AppService {
  @Inject(OtherService)
  private otherService: OtherService;

  getHello(): string {
    return "Hello World!" + this.otherService.xxx();
  }
}

题外话

Strictly speaking,Ruby on Rails 框架没有内置类似 NestJS 或 Spring 那种 IoC 容器(Inversion of Control Container)或严格的依赖注入(DI)机制。

在 NestJS(基于 TypeScript/Node)或 Java Spring 中,IoC 容器是核心,你需要通过 @Injectable()constructor 注入等方式将类交给框架管理。但在 Rails 中,情况截然不同。

以下是关于 Rails 与 IoC/DI 的几个核心结论:

1. 为什么 Rails 没有内置 IoC 容器?

  • 动态语言的特性(Duck Typing):Ruby 是高度动态的语言,没有“接口(Interface)”的概念。在 Java 或 TS 中,依赖注入很大程度上是为了基于接口解耦,方便测试时替换 Mock 对象;而在 Ruby 中,即使你不做依赖注入,也可以在测试时利用动态修改内存库(比如 rspec-mocks)极其容易地把任何类或方法的行为替换掉。
  • Rails 的设计哲学理念:Rails 的创造者 DHH 曾明确发文表示过 “Dependency injection is not a virtue in Ruby”(依赖注入在 Ruby 中并非美德)。Rails 推崇“约定优于配置(Convention over Configuration)”,主张直接调用,认为 DI 引入的样板代码在 Ruby 中弊大于利。
  • Active Record 模式:Rails 深度绑定 Active Record,习惯在 Controller 或 Service 中直接调用全局可见的类,例如直接写 User.find(1) 或者 OrderService.new.call,而不是注入一个 UserRepositoryOrderService 实例。

2. Rails 中怎么处理依赖关系?

虽然没有内置的高级 IoC 容器,但开发者依然可以使用最朴素的方式来解耦(广义上的依赖注入):

  • 构造器注入(PORO):使用普通的 Ruby 对象(Plain Old Ruby Objects),在 initialize 方法中传入依赖。
    1
    2
    3
    4
    5
    
    class StripePaymentGateway
      def initialize(client = Stripe::Client.new) # 默认依赖,也可被外部替换
        @client = client
      end
    end
    

3. 如果硬要在 Ruby/Rails 里用 IoC 容器怎么办?

如果项目非常庞大,且深受领域驱动设计(DDD)或 Clean Architecture 影响,开发者会引入第三方 Gem(第三方库) 来实现类似 NestJS 的 IoC 体验。目前业界最流行的是 dry-rb 生态:

  • dry-systemdry-auto_inject:提供了类似于 IoC 容器的注册表和自动依赖注入功能。