NestJSのProviderとModuleについてまとめてみた

こんばんは、MagicKeboardを買ってキーボードを打つのが好きになってきたmorimorikochanです。

NestJSについて調べる機会があったので、NestJSの大きな特徴であるProviderとModuleについてまとめました

NestJSとは

NestJSとはNodeJSのフレームワークです。2018年ごろに公開されてGitHubのstar数もどんどんん増えていっていることがわかります。

https://star-history.t9t.io/#nestjs/nest

特徴として、Javascript/Typescriptで記述が可能なことや、各種ORMやGraphQLとのintegrationがあることが挙げられます。

また、これ以外にもProvider/Moduleという概念を持っておりそのおかげで疎結合でテスタブルなアプリケーションに保つことができます。

この概念は、名前が違いますが似た概念がSpringBootにも「DIコンテナ」として存在していて、個人的にとても好きです。

Provider/Moduleとは

先にざっくりそれぞれ説明すると、Providerを複数まとめたまとまりをModuleと呼ばれています。

/img/2021-06-24/module.png

Provider

その上で、まずは1つのModule内でのProviderの役割・使い方について説明します。

Providerは公式ドキュメントで

Providers are a fundamental concept in Nest. Many of the basic Nest classes may be treated as a provider – services, repositories, factories, helpers, and so on.

https://docs.nestjs.com/providers

とあるとおり、Providerはもっとも基本的な概念で、様々なクラスがProviderとして扱われます。

NestJSに対してProviderとして登録させるには、

@Injectable()
export class AppService {
}

のように@Injectable()を付与することでNestJS側からProviderとして認識されます。

このようなProviderはProvideの文字通り機能を提供する側なのでそれを利用するクラスも存在します。

利用するクラスでは、以下のように、コンストラクタが利用したいProviderを引数として受け取るように定義します。

export class AppFactory {
  constructor(private appService: AppService) {}
}

じゃあこのクラスを呼び出す側が new AppFactory(new AppService())みたいに、AppServiceをインスタンス化させる必要あるんちゃう?と思われるかもしれないですが、NestJS(およびDIコンテナーを持つFW)では不要です。このnew AppService()はNestJS側が実行してくれます。

インスタンス化による具現化は、クラス間の密結合を引き起こすため、DIコンテナーのしくみを使って開発者自身がインスタンス化を行わずDIコンテナ(NestJS側)が行います。

NestJS側がこの動作を行ってくれるためにはもう一つ記述が必要です。

Moduleの定義時に providersとしてクラスを登録する必要があります。Moduleの細かい話はまだ置いておきます。

@Module({
  providers: [AppService, AppFactory, AppUsecase],
})
export class AppModule {}

こうして定義されたProviderを利用できるクラスは 主に ControllerProviderです。Controllerはここではひとまず置いてきます、肝心なのは、Providerを利用するProviderも存在するということです。

例えば以下のように、AppService→AppFactory→AppUsecaseというような三段構成も可能です。

// app.usecase.ts
@Injectable()
export class AppUsecase {
  constructor(private appFactory: AppFactory) {}

  talk() {
    this.appFactory.makeHello()
  }
}

// app.factory.ts
@Injectable()
export class AppFactory {
  constructor(private appService: AppService) {}

  makeHello() {
    this.appService.getHello()
  }
}

// app.service.ts
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

このようにしてProviderを利用することができます。

今の段階ではInjectする側とInjectされる側がどちらも同じ具象クラスを指しているのであまり旨味を感じないかもしれないですが、インタフェースと組み合わせるとテスタブルで疎結合なProviderができます。

Moduleについて

上でも説明した通り、関連性を持つProviderを複数まとめているものがModuleです。ただしProviderだけではなくControllerも複数まとめています。このModuleという概念は、他のModuleを利用したり利用されたりすることがあります。

例えば公式ドキュメントの図を拝借すると、FeatureModule3ChatModuleに利用されていると同時にChatModuleApplicationModuleに利用されています。

https://docs.nestjs.com/modules

このようにしてModule同士は依存関係を持ちます。Module同士が直接的間接的問わず相互に依存(循環参照)することはNestJSでは避けるべきとされていますが、可能ではあります

あるModuleAが別のModuleBを利用するためには、まずはModuleBが外部モジュールに公開したいProviderをexportします(例ではBServiceをexportします)


// b.module.ts
@Module({
  providers: [BService, BPrivateService],
  exports: [BService]
})
export class ModuleB {}

次に、ModuleAModuleBを以下のようにimportします。

// a.module.ts
@Module({
  providers: [AService],
  exports: [AService]
})
export class ModuleA {}

あとはModuleA内のControllerProviderなどで、同モジュールないのProviderを利用するように外部モジュールであるBServiceを利用することができます。

// a.service.ts
@Injectable()
export class AService {
  constructor(private bService: BService){}
  // ...
}

注意すべき点として、ModuleAModuleBをimportしたからといって、ModuleBの全てのProvider(例で言うとBPrivateService)が使えるようになっていない点が挙げられます

まとめ・雑感

  • Provider
    • NestJSの最小単位の構成要素
    • NestJSのDIコンテナーにて管理される
    • interfaceを活用することによりテスト容易性が上がる(別の記事で説明したい…)
  • Module
    • 特定の関連するProviderやControllerなどを集約した単位
    • 外部へのモジュールに公開するProviderを指定できる
    • 逆に外部のモジュールを取り込み、そのProviderを利用することができる
    • アプリケーションには必ずRootのModuleがある
  • 思ったこと
    • exportがあるために、pythonと違い外部に公開するクラスを制限できるのは良い
    • DIコンテナという概念が途中でゲシュタルト崩壊したので調べてみたらさらにわからなくなった
    • フレームワークの思想をSpringBootに寄せに来ている印象を強く受けた
      • ExceptionFilterとかModuleとかRepositoryの紹介ページとか

See also