Providerとは
Provider
はNestJSの基本的な概念です。NestJSの基本的なクラスの多くはProvider
として扱われることが多いです。Providerの基本的な考えは、依存関係として注入できることです。言い換えれば、オブジェクトはお互いに様々な関係を作れるのです。前の章ではシンプルな
CatsController
を作りました。Controller
はHTTPリクエストを処理し、より複雑なタスクはプロバイダに以上する必要があります。プロバイダとは、Module
の中でプロバイダとして宣言された、JavaScriptクラスです。メモ
NestJSはより強力な方法で依存関係を設計・整理することが可能なので、SOLID原則に従うことを強く推奨します。
まずはシンプルな
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コンテナで管理できるクラスであることを宣言するメタデータをアタッチします。ちなみに、こちらの例ではCat
のinterface
を使っていて、以下のように表記できます。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
メンバの宣言と初期化を同じ場所ですぐに実行できるのです。NestJSは一般的に依存性注入(Dependency Injection, DIパターン)として知られている強力なデザインパターンを中心に構築されています。
Angularの公式ドキュメントによると、DIパターンはクラスが依存性を生成するのではなく、外部のソースから依存性を要求するデザインパターンです。これを活用することで、アプリケーションの柔軟性を向上させ、Module単位でプロダクトを管理しやすくします。
NestJSでは、TypeScriptの機能により、依存関係は型だけで解決されるので、非常に簡単に管理できます。以下の例では、NestJSは
CatsService
のインスタンスを生成して返します。この場合、インスタンスのデータはController
のコンストラクタに返されるか、指定したプロパティに代入されます。constructor(private catsService: CatsService) {}
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
を拡張していない場合は、常にコンストラクタベースのインジェクションを使うことを推奨します。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
を取得するか、あるいはインスタンス化する必要があります。本ページで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()
}
}
Last modified 9mo ago