IoC 是什么解决了什么问题

50 阅读4分钟

问题

很多时候

  • Controller 需要 Service
  • Service 需要 Repository
  • Repository 需要 DataSource
  • DataSource 需要 Config

这叫 依赖图(dependency graph) 。你如果不用任何框架,通常会写成:

// ❌ 手动组装:依赖层层传递,创建顺序必须正确
const config = new Config(process.env)
const ds = new DataSource(config)
const repo = new UserRepository(ds)
const service = new UserService(repo)
const controller = new UserController(service)

问题会越来越明显:

  1. 创建顺序必须对(Config → DataSource → Repo → Service → Controller)
  2. 到处传来传去(每加一个依赖,构造函数都要改)
  3. 不好测试(想测 Service 还得造一堆依赖,或写一堆 fake)
  4. 生命周期难管(DataSource 要复用单例?要连接池?什么时候关闭?)

3种常见做法

A:最直观的“手写 DI 容器”(推荐你先看懂这个)

核心思想:把“怎么创建对象”集中放到一个容器里,业务代码只管“要什么”。

class Container {
  constructor() {
    this.factories = new Map()
    this.singletons = new Map()
  }

  register(name, factory, { singleton = true } = {}) {
    this.factories.set(name, { factory, singleton })
  }

  get(name) {
    if (this.singletons.has(name)) return this.singletons.get(name)
    const item = this.factories.get(name)
    if (!item) throw new Error(`Not registered: ${name}`)
    const instance = item.factory(this)
    if (item.singleton) this.singletons.set(name, instance)
    return instance
  }
}

// ---- 业务对象(只声明依赖,不负责 new 下游)----
class Config {
  constructor(env) { this.env = env }
  get dbUrl() { return this.env.DB_URL }
}

class DataSource {
  constructor(config) { this.config = config }
  query(sql) { return `query(${sql}) @ ${this.config.dbUrl}` }
}

class UserRepository {
  constructor(ds) { this.ds = ds }
  findById(id) { return this.ds.query(`select * from user where id=${id}`) }
}

class UserService {
  constructor(repo) { this.repo = repo }
  getUser(id) { return this.repo.findById(id) }
}

class UserController {
  constructor(service) { this.service = service }
  handle(req) { return this.service.getUser(req.params.id) }
}

// ---- 依赖组装集中在一处 ----
const container = new Container()

container.register('config', () => new Config(process.env))
container.register('dataSource', c => new DataSource(c.get('config')))
container.register('userRepo', c => new UserRepository(c.get('dataSource')))
container.register('userService', c => new UserService(c.get('userRepo')))
container.register('userController', c => new UserController(c.get('userService')))

// 使用时:直接要 Controller
const controller = container.get('userController')
console.log(controller.handle({ params: { id: 1 } }))

你会发现:

  • 创建顺序不再散落在各处,而是集中在容器注册区
  • 依赖新增/替换(比如换 repo)只改注册,不改业务类
  • 单例(DataSource)统一管理

B:工厂函数 + 显式依赖(更轻量,适合小项目)

new 都放到一个 createApp() 里:

function createApp(env) {
  const config = new Config(env)
  const ds = new DataSource(config)
  const repo = new UserRepository(ds)
  const service = new UserService(repo)
  const controller = new UserController(service)
  return { controller }
}

const { controller } = createApp(process.env)

它比到处 new 好,因为依赖组装集中,但不如容器灵活(替换/多实例/生命周期管理没那么方便)

C:使用成熟框架的 DI(NestJS / Spring 这类)

  • 你只“声明依赖”(构造函数要什么)
  • 容器负责“创建对象并把依赖塞进去”
  • 容器也负责生命周期(单例、请求级、关闭钩子)

以 NestJS 的思路(示意)就是:

  • @Controller() 里注入 UserService
  • @Injectable()UserService 注入 UserRepository
  • UserRepository 再注入 DataSource/Config

(框架帮你自动按依赖图排序创建)

image.png

比如这样声明 AppController 依赖了这两个 Service,然后让工具分析依赖自动帮我创建好这三个对象并设置依赖关系

这就是 IoC 的实现思路

有一个放对象的容器,程序初始化的时候会扫描 class 上声明的依赖关系,然后把这些 class 都给 new 一个实例放到容器里。

创建对象的时候,还会把它们依赖的对象注入进去。这样就完成了自动的对象创建和组装

Nest具体如何做的

image.png

有一个 AppService 声明了 @Injectable,代表这个 class 可注入,那么 nest 就会把它的对象放到 IOC 容器里

image.png

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

AppController 的构造器参数依赖了 AppService。

或者这样通过属性的方式声明依赖:

image.png

前者是构造器注入,后者是属性注入,两种都可以。

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

因为 Service 是可以被注入也是可以注入到别的对象的,所以用 @Injectable 声明。

而 Controller 只需要被注入,所以 nest 单独给它加了 @Controller 的装饰器。

然后在 AppModule 里引入:

image.png

通过 @Module 声明模块,其中 controllers 是控制器,只能被注入。

providers 里可以被注入,也可以注入别的对象,比如这里的 AppService

入口模块使用

image.png

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

image.png

AppController 只是声明了对 AppService 的依赖,就可以调用它的方法了:

image.png

nest 在背后自动做了对象创建和依赖注入的工作

nest 还加了模块机制,可以把不同业务的 controller、service 等放到不同模块里,这个之前提过

比如

nest g module moduleA

image.png

生成这个模块代码

image.png

并更新了 app.module.ts 更新了这个 imports

image.png

如果生成一个 service

nest g service serviceA

image.png

生成代码

image.png

会添加到 providers 中

image.png

仓库

github.com/huanhunmao/…