NestJSに入門してみる(DI編)

前回の記事でNestJSに入門しました。

今回はその続きです。タイトルにある通りDIについて書いていきます。

DIとは

その前に軽くDIについておさらいしておきます。

DIはDependency Injectionの略で日本語に訳すと依存性の注入です。

例を挙げると、

class Hoge {
  function hoge() {
    console.log('hogehoge');
  }
}

class Fuga {
  function fuga() {
    const hoge = new Hoge();
    hoge.hoge();
  }
}

const fuga = new Fuga();
fuga.fuga();

上記の例の場合、Fugaクラスのfuga()という関数はHogeクラスを内部でインスタンス化しているため、Hogeクラスに深く依存してしまっています。

こうなると密結合でテストも書きづらいです。

そもそもHogeクラスのインスタンスを生成する責務はFugaクラスには本来無いはずです。

なので、依存するクラスを外部から渡してあげる。これが依存性の注入です。

方法としてはコンストラクタインジェクションやセッターインジェクション等があります。

// コンストラクタインジェクション
class Hoge {
  hoge() {
    console.log('hogehoge');
  }
}

class Fuga {
  constructor(hoge) {
    this.hoge = hoge;
  }

  fuga() {
    this.hoge.hoge();
  }
}

const fuga = new Fuga(new Hoge());
fuga.fuga();
// セッターインジェクション
class Hoge {
  hoge() {
    console.log('hogehoge');
  }
}

class Fuga {
  setHoge(hoge) {
    this.hoge = hoge;
  }

  fuga() {
    this.hoge.hoge();
  }
}

const fuga = new Fuga();
fuga.setHoge(new Hoge());
fuga.fuga();

個人的にはコンストラクタインジェクションの方が良いと思います。

セッターインジェクションの場合、Fugaクラスのインスタンスを生成した時点ではhogeプロパティには何も入っておらずsetHogeが呼ばれないとエラーになるためです。

インスタンス化した時点で必要なものがすべて入った状態を作る(=完全コンストラクタ)のが設計としても良いと思います。

NestJSでのDI

NestJSでのDIは前回の記事で実は少し触れています。

app.service.tsは以下のようになっています。

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

@Injectable()というデコレータが付いており、これによって依存関係として注入できるクラスであることを示します。

一方、app.module.tsはこちら。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

providersで先程のAppServiceを指定しています。

これによって、AppModuleの中ではAppServiceを注入することができます。

ちなみに省略せずに書くとこのようになります。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      useClass: AppService
    }
  ],
})
export class AppModule {}

useClassで実際に呼び出されるクラスを指定し、provideで何という名前で扱うかを指定します。

抽象に依存したい

しかしこのままでは具象クラスを直接注入することになります。

SOLID原則のDに当たる依存性逆転の法則では「抽象に依存すべきである」とされています。

app.controller.tsを見ると、

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('cats')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

AppControllerクラスはAppServiceクラスに依存しています。

AppServiceクラスではなく他のクラスを使うことになった場合、AppControllerクラス自体も修正が必要です。

そうならないためにinterfaceや抽象クラスを用意してAppServiceクラスはそれを実装または継承する形にし、AppControllerクラスはその抽象に依存させる形が望ましいです。

NestJSでのやり方を調べると公式に以下の項目が書いてありました。

Non-class-based provider tokens

これを読むと、以下2点で実現できそうです。

  • AppModuleprovidetokenと呼ばれる文字列を指定する
  • 依存注入するクラスで@inject()デコレータを使い、↑のtokenを指定する

というわけでやってみます。以下のinterfaceを作りました。

export interface AppServiceInterface {
  getHello(): string;
}

そして具象クラスも上記のinterfaceを実装する形に修正します。

import { Injectable } from '@nestjs/common';
import { AppServiceInterface } from './app.service.interface';

@Injectable()
export class AppService implements AppServiceInterface {
  getHello(): string {
    return 'Hello World!';
  }
}

AppModuleも以下の通り修正。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: 'SERVICE',
      useClass: AppService
    }
  ],
})
export class AppModule {}

そして最後にAppController@inject()を使います。

import { Controller, Get, Inject } from '@nestjs/common';
import { AppServiceInterface } from './app.service.interface';

@Controller('cats')
export class AppController {
  constructor(@Inject('SERVICE') private readonly appService: AppServiceInterface) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

修正後のコードでも無事Hello Worldできました。

実際には公式の例にあるようにtokenを定数ファイルで管理する方法が良さそうですね。

まとめ

以上、NestJSでのDIについて書きました。

普段LaravelではServiceProviderを使っているのでNestJSでも同じことができるのは嬉しいです。

そして改めてTypeScriptの恩恵も感じました。TypeScript万歳。