カスタムのプロバイダ

カスタムプロバイダ

以前の章では、依存性注入(DI)の様々な側面と、それがNestでどのように活用されているのかについて言及しました。その一例として、インスタンス(多くの場合サービスプロバイダ)をクラスに注入するために使用される、コンストラクタベースの依存性注入があります。アプリケーションがより複雑になると、DIシステムの全機能を利用する必要が出てくるかもしれません。

依存性注入(DI)とは

依存性注入は、制御の逆転(IoC, Inversion of Control)のテクニックの1つで、依存性のインスタンス化を自分のプログラムで強制的に行うのではなく、IoCコンテナに委ねるというものである。Providersのセクションにあるこの例で起こっていることを見てみましょう。
最初に、プロバイダを定義します。@Injectable()デコレータはCatsServiceクラスをプロバイダとしてマークします。
cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}
そして、Nestにプロバイダをコントローラクラスにインジェクトするように依頼します。
cats.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
最後に、NestJSのIoCコンテナにプロバイダを登録します。
app.module.ts
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 {}
これを実現するために、具体的にどのようなことがプログラムの裏側で起こっているのでしょうか?そこには、3つの重要なステップがあります。
  1. 1.
    cats.service.tsでは、@Injectable()デコレータで、CatsServiceクラスをNest IoCコンテナが管理できるクラスとして宣言しています。
  2. 2.
    cats.controller.tsで、CatsControllerCatsServiceトークンへの依存をコンストラクタ注入で宣言しています。
  3. 3.
    app.module.tsで、CatsServiceトークンを、cats.service.tsファイルにあるCatsServiceクラスに関連付けている。この関連付けがどのように行われるかはもう少し後で解説します。
constructor(private catsService: CatsService)
Nest IoCコンテナが最初にCatsControllerをインスタンス化する際に、まず依存関係を探します。CatsServiceという依存関係が見つかると、CatsServiceトークンを検索してCatsServiceクラスを返します。
デフォルトの動作を想定すると、NestはCatsServiceのインスタンスを生成してキャッシュ化し、それを返すか、あるいはすでにキャッシュされている場合は既存のインスタンスを返すかのどちらかにななります。

標準プロバイダ

@Module()デコレータを確認しましょう。
app.module.ts
@Module({
controllers: [CatsController],
providers: [CatsService],
})
providersプロパティは、プロバイダの配列を受け取ります。これまでのところ、クラス名のリストを通してプロバイダを提供しました。実際、providers.CatsService構文は、より完全な構文であるCatsServiceの短縮形です。
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
このように明示的に構築することで、登録する処理を理解できます。ここでは、CatsServiceトークンをCatsServiceクラスに関連付けています。この短縮表記は、もっとも一般的な使用例であるトークンを使って同名のクラスのインスタンスを要求する実装を簡略化しているだけです。

カスタムプロバイダ

標準的なプロバイダが提供する以上の要件がある場合はどうすればいいのでしょうか?以下はその一例になります。
  • NestJSがクラスをインスタンス化する
  • 既存のクラスを2つ目の依存関係で再利用しする
  • テスト用にモックバージョンを使ってクラスを継承する
NestJSでは、このようなケースに対応するため、カスタムプロバイダを定義できます。カスタムプロバイダを定義するには、いくつかの方法があります。
メモ
依存関係に問題がありる場合は、NEST_DEBUG環境変数を設定し、起動時に追加の依存関係解決ログを取得できます。

useValue

useValue構文は定数値を代入したり、外部ライブラリをNestコンテナに入れたり、実際の実装をモックオブジェクトに置換したりする際に便利です。例えば、テスト用にCatsServiceのもっくをNestに強制的に使用させたいことを想定します。
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
この例では、CatsServiceトークンはmockCatsServiceモックオブジェクトを解決します。useValueは値を必要とし、この場合は置換されるCatsServiceクラスと同じインターフェイスを持つリテラルオブジェクトを使用します。TypeScriptの構造的な型付けによって、リテラルオブジェクトあるいはnewでインスタンス化したクラスインスタンスなど、互換性のあるインターフェイスを持つオブジェクトなら何でも使えます。

クラスベースではないプロバイダトークン

これまでのところ、プロバイダトークンにはクラス名を使用してきました。これは、コンストラクタによるインジェクションで使用される標準的なパターンと一致しており、トークンはクラス名でもあります。ときには、文字列や記号をDIトークンとして仕様すつ柔軟性も必要です。例えば、以下のように実装できます。
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
上記の例では、文字列値のトークン('CONNECTION')を、外部ファイルからインポートした既存の接続オブジェクトと関連付けています。
確認
トークン値にTypeScriptの列挙型(enum)を活用できます。
'CONNECTION'カスタムプロバイダーは文字列値トークンを使用します。このようなプロバイダを注入するには、@Injectable()デコレータを使用します。このデコレータは単一の引数であるトークンを受け取ります。
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
メモ
@Injectable()デコレータは@nestjs/commonからインポートできます。
上記では、説明のために文字列'CONNECTION'を直接使用していますが、きれいなコード構成のためには別のファイルでトークンを定義しましょう。

useClass

useClass構文を使用すると、トークンの解決先となるクラスを動的に決定づけられます。たとえば、抽象的な(あるいはデフォルトの)ConfigServiceクラスがあるとします。現在の環境に応じて、NestJSが異なる実装の設定サービスを提供するように実装します。
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
上記のコードサンプルの詳細をいくつか見てみましょう。最初にconfigServiceProviderをリテラルオブジェクトで定義し、それをモジュールデコレータのprovidersプロパティに渡すことがわかります。
また、ConfigServiceのクラス名をトークンとして使っています。ConfigServiceに依存するクラスに対して、NestJSは提供されたクラスのインスタンスを注入し、他のファイルで宣言されたデフォルトの実装をオーバーライドしています。

useFactory

useFactory構文はプロバイダを動的に作成できます。実際のプロバイダは、ファクトリー関数から返される値によって提供されます。ファクトリー関数は必要なだけ単純でも複雑でもかまいません。単純なファクトリーでは、他のプロバイダに依存することはありません。より複雑なファクトリーは、結果を計算するために必要な他のプロバイダを自分で注入できます。
ファクトリー関数では、(オプションの)引数を受け取れます。injectプロパティは、NestJSが解決するプロバイダの配列を受け入れ、インスタンス化プロセスでファクトリー関数に引数として渡します。
const connectionProvider = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
// \_____________/ \__________________/
// This provider The provider with this
// is mandatory. token can resolve to `undefined`.
};
@Module({
providers: [
connectionProvider,
OptionsProvider,
// { provide: 'SomeOptionalProvider', useValue: 'anything' },
],
})
export class AppModule {}

useExisting

useExisting構文を使用すると、既存のプロバイダのエイリアスを作成できます。これによって、同じプロバイダにアクセスするための方法が2つ作成されます。
以下の例では、'AliasedLoggerService'はトークンLoggerServiceのエイリアスです。AliasedLoggerServiceLoggerService用の2つの異なる依存関係があるとします。両方の依存関係がSINGLETONスコープで指定されている場合は、両方とも同じインスタンスに解決されます。
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

サービスに基づいていないプロバイダ

プロバイダはサービスを提供することが多いですが、その使い方に限定されるものではありません。\プロバイダはどんな値でも提供できます。例えば、以下のようにプロバイダは現在の環境に基づいて構成オブジェクトの配列を提供できます。
const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
},
};
@Module({
providers: [configFactory],
})
export class AppModule {}

カスタムプロバイダをエクスポートする

他のプロバイダと同様に、カスタムプロバイダは宣言したモジュールにスコープされます。他のModuleから見えるようにするためには、カスタムプロバイダをエクスポートしておく必要があります。
カスタムプロバイダをエクスポートするには、そのトークンまたは完全なプロバイダオブジェクトを使用します。
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}