カスタムのプロバイダ
以前の章では、依存性注入(DI)の様々な側面と、それがNestでどのように活用されているのかについて言及しました。その一例として、インスタンス(多くの場合サービスプロバイダ)をクラスに注入するために使用される、コンストラクタベースの依存性注入があります。アプリケーションがより複雑になると、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.
cats.service.ts
では、@Injectable()
デコレータで、CatsService
クラスをNest IoCコンテナが管理できるクラスとして宣言しています。 - 2.
cats.controller.ts
で、CatsController
がCatsService
トークンへの依存をコンストラクタ注入で宣言しています。 - 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
構文は定数値を代入したり、外部ライブラリを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構文を使用すると、トークンの解決先となるクラスを動的に決定づけられます。たとえば、抽象的な(あるいはデフォルトの)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
構文はプロバイダを動的に作成できます。実際のプロバイダは、ファクトリー関数から返される値によって提供されます。ファクトリー関数は必要なだけ単純でも複雑でもかまいません。単純なファクトリーでは、他のプロバイダに依存することはありません。より複雑なファクトリーは、結果を計算するために必要な他のプロバイダを自分で注入できます。ファクトリー関数では、(オプションの)引数を受け取れます。
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構文を使用すると、既存のプロバイダのエイリアスを作成できます。これによって、同じプロバイダにアクセスするための方法が2つ作成されます。
以下の例では、
'AliasedLoggerService'
はトークンLoggerService
のエイリアスです。AliasedLoggerService
とLoggerService
用の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 {}
Last modified 7mo ago