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点で実現できそうです。
AppModule
のprovide
にtoken
と呼ばれる文字列を指定する- 依存注入するクラスで
@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万歳。