Providerとは

Providerとは

ProviderはNestJSの基本的な概念です。NestJSの基本的なクラスの多くはProviderとして扱われることが多いです。Providerの基本的な考えは、依存関係として注入できることです。言い換えれば、オブジェクトはお互いに様々な関係を作れるのです。
前の章ではシンプルなCatsControllerを作りました。ControllerはHTTPリクエストを処理し、より複雑なタスクはプロバイダに以上する必要があります。プロバイダとは、Moduleの中でプロバイダとして宣言された、JavaScriptクラスです。
メモ
NestJSはより強力な方法で依存関係を設計・整理することが可能なので、SOLID原則に従うことを強く推奨します。

Service

まずはシンプルなCatsServiceを作成しましょう。このサービスは、データの保存と取得を担当し、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
}
}
メモ
Nest CLIでServiceを作成する時、以下のコマンドを実行してください。 nest g service cats
今回作成したCatsServiceとは、一つのプロパティと2つのメソッドを持つ基本的なクラスです。唯一の新しい特徴は、@Injectable()デコレータを使っていることです。@Injectable()デコレータは、CatsServiceがNest IoCコンテナで管理できるクラスであることを宣言するメタデータをアタッチします。ちなみに、こちらの例ではCatinterfaceを使っていて、以下のように表記できます。
interfaces/cat.interface.ts
export interface Cat {
name: string;
age: number;
breed: string;
}
Catを取得するための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は、クラスのコンストラクタから注入されます。private構文が使われていることに注目してください。この省略気泡により、CatsServiceメンバの宣言と初期化を同じ場所ですぐに実行できるのです。

依存性注入(Dependency Injection)

NestJSは一般的に依存性注入(Dependency Injection, DIパターン)として知られている強力なデザインパターンを中心に構築されています。
Angularの公式ドキュメントによると、DIパターンはクラスが依存性を生成するのではなく、外部のソースから依存性を要求するデザインパターンです。これを活用することで、アプリケーションの柔軟性を向上させ、Module単位でプロダクトを管理しやすくします。
NestJSでは、TypeScriptの機能により、依存関係は型だけで解決されるので、非常に簡単に管理できます。以下の例では、NestJSはCatsServiceのインスタンスを生成して返します。この場合、インスタンスのデータはControllerのコンストラクタに返されるか、指定したプロパティに代入されます。
constructor(private catsService: CatsService) {}

スコープ(Scope)

Providerは通常、アプリケーションのライフサイクルと動悸した寿命を持っています。これをスコープと呼びます。アプリケーションが起動された時、アプリケーション内の依存関係は全て解決されなければなりません。したがって、全てのProviderはインスタンス化されなければなりません。同様に、アプリケーションがシャットダウンする時、各Providerは破壊されます。

Providerをカスタマイズする方法

NestJSには、Provider間の関係を解決する反転制御コンテナが組み込まれています。こちらの機能は、前述の依存性注入機能の根底にあるものですが、実際にはこれまで説明したものよりもはるかに強力です。プロバイダを定義する方法はいくつかありますが、プレーンな値、クラス、非同期または同期のFactoryを使用できます。
時には、必ずしも解決する必要のない依存関係が存在することもあります。例えば、クラスが設定されたオブジェクトに依存しているが、何も渡されない場合はデフォルト値が使われる場合があります。このような場合は、設定プロバイダがなくてもエラーにならないので、依存関係はオプションになります。
Providerがオプションであることを示すためには、@Optional()デコレータを使います。
import { Injectable, Optional, Inject } from '@nestjs/common'
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
上記のプログラムでは、カスタムのProviderを使っているので、HTTP_OPTIONSカスタムトークンが含まれていることには十分に注意してください。これまでの例では、コンストラクタ内のクラスを通じて依存関係を示すコンストラクタベースのインジェクションを示しましたが、今回の例では、コンストラクタ内のクラスを通じて依存関係を示すインジェクションを示します。

プロパティベースのインジェクション

先程のコーディング手法は、Providerがコンストラクタ経由で注入されるので、コンストラクタベースのインジェクションと呼ばれています。非常に特殊なケースでは、プロパティベースのインジェクションが有効な場合もあります。例えば、トップレベルのクラスが一つあるいは複数のプロバイダに依存している場合、コンストラクタからサブクラスのsuper()を呼び出してProviderを渡すのは非常に面倒です。これを避けるために、プロパティ単位で@Inject()デコレータを使うことを推奨します。
import { Injectable, Inject } from '@nestjs/common'
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T
}
注意
クラスが他のProviderを拡張していない場合は、常にコンストラクタベースのインジェクションを使うことを推奨します。

Providerの登録

CatsService(Provider)とCatsController(Consumer)をそれぞれ定義したので、NestJSにサービスを登録し、インジェクションを実行できるようにする必要があります。Moduleファイル(app.module.ts)を編集して、@Module()デコレータのProvider配列にサービスを追加することでこれを実行します。
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 {}
これで、NestJSはCatsControllerクラスの依存関係を解決できるようになります。
ディレクトリは以下の通りです。
src
 cats
  dto
   create-cat.dto.ts
  interfaces
   cat.interface.ts
  cats.controller.ts
  cats.service.ts
 app.module.ts
 main.ts

手動でインスタンス化

これまで、NestJSが依存関係を解決するためのほとんどの詳細を自動的に処理する方法について詳細に解説しました。状況によっては、組み込みのDIシステムの対象外で、手動でProviderを取得するか、あるいはインスタンス化する必要があります。
既存のインスタンスを取得したり、Providerのインスタンスを動的に作成したりする際には、Moduleとはのページを参照してください。

SOLID原則とは

本ページでSOLID原則について軽く触れておきます。SOLID原則とは、次の5つの言葉の頭文字を取ったものになります。
  • S(Single Responsibility Principle):単一責任の原則
  • O(Open/Closed principle): 開放閉鎖の原則
  • L(Liskov substitution principle): リスコフの置換原則
  • I(Interface segregation principle): インターフェース分離の原則
  • D(Dependency inversion principle): 依存性逆転の原則
簡単に言えば、SOLID原則はオブジェクト指向プログラミングにおいて変更しやすい・理解しやすい・再利用しやすいモジュールを設計・開発するための原則を意味します。
それぞれ順番に解説していきます。

単一責任の原則

単一責任の原則とは、モジュールは一つの役割を果たすべきだという原則です。
class Employee {
name() { // NG: データは別に持つべき
return 'Kodak'
}
department(dept: DepartmentInterface) { // OK: データは別クラスで持つ
return dept.name()
}
}
interface DepartmentInterface {
name(): string
}
class SystemEngineer implements DepartmentInterface {
name() {
return 'SE'
}
}

開放閉鎖の法則

解放閉鎖の法則とは、ソフトウェアの振る舞いが既存の成果物を変更せずに拡張できるようにするべきだという法則を意味します。
class Printer {
print(printer: PrinterInterface) {
return printer.print()
}
}
interface PrinterInterface {
print(): void;
}
// 機能を追加したいときは、以下「Printer」クラスを増やすだけ。
class DefaultPrinter implements PrinterInterface {
print() {
// ...
}
}
class HtmlPrinter implements PrinterInterface {
print() {
// ...
}
}
class TextPrinter implements PrinterInterface {
print() {
// ...
}
}

リスコフの置換原則

リスコフの置換原則とは、クラスを設計する前にあらかじめinterfaceで設計する対象を明確にすることを意味します。
// rectangleには長方形 or 正方形が入る。
// 長方形は幅と高さを独立して変更できるのに対して、正方形は両方同時に変更する必要がある。
// user関数からみるとこれが判断できない
function user(rectangle: Rectangle){
// ...
}
class Rectangle {
constructor(protected height: number, protected width: number) {
}
setH(set: number) {
this.height = set;
}
setW(set: number) {
this.width = set;
}
}
class Square extends Rectangle {
constructor(height: number, width: number) {
super(height, width)
}
setSide(set: number) {
this.height = set;
this.width = set;
}
}
user(new Rectangle(1,2))
user(new Square(2,2)) // 注意: 継承しているのでSquareでも呼び出せる

インターフェイス分離の法則

これを簡単に言えば、使っていないメソッドを持つクラスに依存してはいけないという法則です。
interface AnimalInterface {
sleep(): void
eat(): void
fly(): void
}
class Human implements AnimalInterface
{
sleep() {}
eat() {}
fly() {} // Humanクラスでは、fly()は使用しない
}

依存性逆転の法則

これは、クラスを呼び出すときは抽象クラス(Abstract)を呼び出すようにという原則です。
class Animal {
createPenguin() {
const penguin = new Penguin() // AnimalクラスからPenguinクラス(具象)へ依存
penguin.swim()
}
}
// 上の方法ではなく、抽象を使う
class Animal {
createPenguin(penguin: PenguinInterface) { // Animal → PenguinInterface ← Penguin へと依存関係を変える。
penguin.swim()
}
}