はじめに
NestJS ガチ入門シリーズ 第3弾です。
前回までの記事は以下のとおりです。
今回参照する公式ドキュメントは以下の Provider 編になります。
今回もほぼ翻訳という形で進めていきます。
Provider とは
Provider とは NestJS の核となる概念 (fundamental concept) です。
NestJS で作成する Service, Repository, Factory, Helper など様々なクラスをプロバイダーとして扱うことができます。
Provider は依存関係として注入されることが可能であること、というのが Provider の主要な考えになります。
- オブジェクト同士は、互いに様々な関係を持つことができる
- これらのオブジェクトを「接続する」 ("wiring up") 機能は主に NestJS に委譲できる
前回の章( Controller 編)では、シンプルな CatsController
を作成しました。
コントローラは HTTP リクエストを扱うべきであり、さらに複雑な処理は Provider に委譲すべきです。
Privider は module 内で providers
として宣言される生の (plain) JavaScript クラスです。
NestJS によりオブジェクト指向な方法で依存関係の設計・整理できるようになるため、 SOLID 原則に従うことを強くおすすめします。
Services
CatsService
を作るところから始めましょう。
この Service はデータの保存・取得の責務を担います。
そして、 CatsController
から使われるよう設計されているため、 Provider として定義される良い例です。
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
CatsService
は 1 つのプロパティと 2 つのメソッドを持つ、基本的なクラスです。
唯一の新しい特徴としては、 @Injectable()
デコレータを使っていることです。
@Injectable()
デコレータは、 CatsService
が Nest の IoC コンテナで管理可能なクラスであることを宣言するメタデータを付与します。
ちなみに、この例では Cats
インターフェイスを使用しており、これは以下のようになります。
export interface Cat {
name: string;
age: number;
breed: string;
}
そして、この Service クラスを使用する CatsController
は以下のようなものになります。
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
CatsService
はクラスコンストラクターを介して注入 (inject) されます。
ここで、 private
構文を使っていることに注意してください。
この短い書き方によって、 catsService
メンバの宣言および初期化を即座に同じ場所で行うことができます。
Dependency Injection
Nest は Dependency Injection として知られる、強力な設計パターンを中心に構築されています。
これについては Angular の公式ドキュメントを読むことをおすすめします。
Nest では、 TypeScript の能力によって、依存関係が型によって解決されるため、非常に簡単に依存関係を管理することができます。
以下の例では、 Nest は CatsService
のインスタンスを作成し返すことで、 catsService
を解決します。
(もしくはシングルトンの一般的なケースでは、他の場所ですでにリクエストされていた場合、既存のインスタンスを返します。)
constructor(private catsService: CatsService) {}
Scopes
Provider は基本的にアプリケーションのライフサイクルと同期されたライフタイム( "scope" )を持ちます。
アプリケーションが立ち上がる( bootstrap )とき、すべての依存関係が解決されている必要があり、それにともなってすべての Provider がインストールされる。
同様に、アプリケーションが終了するとき各 Provider は破棄される。
しかし、 Provider のライフタイムをリクエスト単位(request-scoped)にすることもできます。
これらの詳細な手法はこちらをご参照ください。
Custom Providers
NestJS は Provider 間の関係を解決する IoC (inversion of control: 制御の反転) コンテナが組み込まれています。
これは上記で触れた依存関係の注入の基礎になりますが、実際にはこれまで触れてきた内容よりもっとパワフルなものです。
生 (plain) の値、クラス、非同期的もしくは同期的なファクトリーなどを使って Provider を定義することができます。
より多くの例はこちらからご確認ください。
Optional Providers
解決する必要のない依存を持つプロバイダーの場合、 @Optional()
デコレータをコンストラクタのシグネチャで指定する。
例えば、そのクラスが設定オブジェクト (Configuration Object) に依存しているけれど、それが渡されなかった場合、デフォルト値を使えばよい場合など。
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
上記の例では、 Custom Provider を使用していることに注意してください。
これは HTTP_OPTIONS
という Custom Token を含めているためです。
以前の例では、コンストラクタベースの依存性注入を見てきました。
Custom Provider とそれに紐づけられる Token についてはこちらをご覧ください。
Property-based injection
ここまではコンストラクタメソッドにプロバイダが注入される constructor-based injection を見てきました。
ごくまれに、 property-based injection が役立ちます。
例えば、上位クラスが1つ以上のプロバイダーに依存している場合、すべてのサブクラスのコンストラクタから super()
メソッドを呼び出していくのはめんどくさいです。
これを避けるために、 @Inject()
デコレータをプロパティレベルで呼ぶことが可能です。
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
もしクラスが他のクラスを継承(extend)していない場合、基本的に constructor-based injection を使用すべきです。
Provider registration
これまでに CatsService
を定義し、さらにそのサービスを使用する CatchController
があります。
そして、サービスを使用するためにサービスを登録する必要があり、それは注入を行うことで可能です。
これはモジュールファイル( app.module.ts
)内の @Module()
デコレータの providers
配列にサービスを追加することで行えます。
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
これによって Nest は CatsController
クラスの依存関係を解決できるようになります。
このときのディレクトリ構造は以下のとおりです。
src
├ cats
│ ├ dto
│ │ └ create-cat.dto.ts
│ ├ interfaces
│ │ └ cat.interface.ts
│ ├ cats.controller.ts
│ └ cats.service.ts
├ app.module.ts
└ main.ts
Manual instantiation
ここまで、 Nest がどのようにして自動的に依存関係を解決するのかの詳細を見てきました。
特定の状況では、備え付けの DI (依存関係注入)システムの外にて手動でプロバイダーを取得、もしくはインスタンス化する必要があります。
これは、以下の2種類の方法で行うことができます。
- Module reference: 動的に Provider のインスタンスを取得もしくは生成するときに使用できます
-
Standalone applications:
bootstrap()
内で Provider を取得したいときに使用できます- 例: Controller を使用しないスタンドアロンなアプリケーション、立ち上げ時に configuration service を使用する場合
まとめ
今回もドキュメントをなぞるだけになってしまいましたが、個人的には問題解決ベースで読む際より知らないことが知れて良かったと思います。
特に、 Optional Provider なるものがあるのは知れて良かったです。