IOC

为什么需要 IOC

后端系统中,会有很多种对象,它们分别有不同的作用:

  1. Controller 对象:用来接收 HTTP 请求,并且调用 Service 层,返回响应
  2. Service 对象:实现业务逻辑
  3. Repository 对象:放置在 Service 层中抽象的数据库增删改查逻辑
  4. Config 对象:存放配置的对象
  5. DataSource 对象:连接数据库
  6. ...等等

这些对象之间存在错综复杂的关系:

Controller 中调用 Service、Service 调用 Repository 对象,Repository 对象使用 DataSource 对象做数据库的连接,DataSource 又依赖了 Config 对象获取数据库相关信息...

所以我们创建对象的顺序就非常重要:

config --> DataSource --> repository --> service --> controller

在应用初始化的时候,需要理清依赖的先后关系,创建一大堆对象组合起来,这种方式非常麻烦。

于是,有一种叫 IOC (Inverse Of Control),中文名字 反转控制的解决方案出来了。

什么是 IOC

现在我们有很多种 class 对象,我们能不能再 class 对象上加一种注解,代表这个 class 需要依赖什么,然后让工具自动分析依赖关系,然后自动将这些对象组装起来呢?

这就是 IOC 的实现思路。

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

创建对象的时候,还会把它们依赖的对象注入进去。

这样不就完成了自动的对象创建和组装么?

这种依赖注入的方式叫做 Dependency Injection,简称 DI。

原本需要手动创建依赖到现在只需要声明依赖了啥,等待被注入,这就是 Inverse Of Control,反转控制。

Nest 中的 IOC

Nest 中声明依赖的方式,是装饰器。

我们先前创建的的 Controller 上就有一个装饰器:

ts
1@Controller()
2export class AppController {}

在默认的app.service.ts中,也有一个装饰器:

ts
1@Injectable()
2export class AppService {
3  getHello(): string {
4    return 'Hello World!'
5  }
6}

上面两个装饰器都代表这些 class 对象需要放到统一的 IOC 容器中:

  • @Injectable()表示这个 class 是可以被注入的,同时它也可以注入其他依赖的。
  • @Controller比较特殊,它表示这个 class 可以被注入,但因为它是接收 http 请求的第一层对象,不需要注入到其他依赖。

然后这两个对象需要在 AppModule 里引入:

ts
1@Module({
2  imports: [],
3  controllers: [AppController],
4  providers: [AppService],
5})

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

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

这个模块最终会在入口文件main.ts中跑起来:

ts
1import { NestFactory } from '@nestjs/core'
2import { AppModule } from './app.module'
3
4async function bootstrap() {
5  const app = await NestFactory.create(AppModule)
6  await app.listen(3000)
7}
8bootstrap()

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

当它发现这段代码:

ts
1@Controller()
2export class AppController {
3  constructor(private readonly appService: AppService) {}
4}

于是它就会创建 AppService 对象并且注入到 AppController 对象中,现在就可以调用 AppService 中定义的方法了:

ts
1@Controller()
2export class AppController {
3  constructor(private readonly appService: AppService) {}
4
5  @Get()
6  getHello(): string {
7    return this.appService.getHello()
8  }
9}

我们只需要做依赖声明,Nest 背后会自动创建实例并且把依赖注入进去,这种开发方式是不是很方便呢?

注入其他模块

Nest 有模块机制,可以把不同业务的 controller 和 service 等放到不同模块里。

想要使用其他业务模块的内容,可以先把那个模块给 import 进来。

ts
1// AppModule 引入了 GamesModule
2@Module({
3  imports: [GamesModule],
4  controllers: [AppController, UserController],
5  providers: [AppService],
6  exports: [AppService],
7})
8export class AppModule {}

当 import 别的模块后,那个模块 exports 的 provider 就可以在当前模块注入了。

tsx
1// GamesModule 导出了 GamesService
2@Module({
3  controllers: [GamesController],
4  providers: [GamesService],
5  exports: [GamesService],
6})
7export class GamesModule {}

现在导出的 GamesService中有一个这样的方法:

ts
1@Injectable()
2export class GamesService {
3  findAll() {
4    return `This action returns all games`
5  }
6}

那么我在AppModule中就可以使用该模块导出的依赖,比如在 Controller 中注入该依赖:

ts
1import { Controller, Get } from '@nestjs/common'
2import { AppService } from './app.service'
3import { GamesService } from './games/games.service'
4
5@Controller()
6export class AppController {
7  constructor(
8    private readonly appService: AppService,
9    private readonly gamesService: GamesService, // 注入 GamesService
10  ) {}
11}

并且使用它的方法:

ts
1  @Get()
2  getAllGame(): string {
3    return this.gamesService.findAll();
4  }

现在打开http://localhost:3000/,能看到来自其他模块的 GamesService 返回的结果了:

html
1This action returns all games

总结

IOC 是一种解决后端依赖对象过多,手动创建和组装这些对象过于繁琐的方案。

我们只需要在 class 上用装饰器标识它可以被注入,然后声明它的依赖是什么。框架背后的 IOC 机制会帮助我们自动创建和组装对象。

体现在 Nest 里是这样的:

  • 通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。

  • Nest 会从入口文件main.ts扫描模块里的这些对象和依赖,背后自动创建、注入、组装对象。

Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。