0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【NestJS】ガチ入門【その3: Provider 編】

Posted at

はじめに

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 として定義される良い例です。

cats.service.ts
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 インターフェイスを使用しており、これは以下のようになります。

interfaces/cat.interface.ts
export interface Cat {
  name: string;
  age: number;
  breed: string;
}

そして、この Service クラスを使用する CatsController は以下のようなものになります。

cats.controller.ts
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 なるものがあるのは知れて良かったです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?