こんばんは、MagicKeboardを買ってキーボードを打つのが好きになってきたmorimorikochanです。
NestJSについて調べる機会があったので、NestJSの大きな特徴であるProviderとModuleについてまとめました
NestJSとは
NestJSとはNodeJSのフレームワークです。2018年ごろに公開されてGitHubのstar数もどんどんん増えていっていることがわかります。
特徴として、Javascript/Typescriptで記述が可能なことや、各種ORMやGraphQLとのintegrationがあることが挙げられます。
また、これ以外にもProvider/Moduleという概念を持っておりそのおかげで疎結合でテスタブルなアプリケーションに保つことができます。
この概念は、名前が違いますが似た概念がSpringBootにも「DIコンテナ」として存在していて、個人的にとても好きです。
Provider/Moduleとは
先にざっくりそれぞれ説明すると、Providerを複数まとめたまとまりをModuleと呼ばれています。
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.
とあるとおり、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を利用できるクラスは 主に Controller
やProvider
です。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を利用したり利用されたりすることがあります。
例えば公式ドキュメントの図を拝借すると、FeatureModule3
はChatModule
に利用されていると同時にChatModule
はApplicationModule
に利用されています。
このようにしてModule
同士は依存関係を持ちます。Module
同士が直接的間接的問わず相互に依存(循環参照)することはNestJSでは避けるべきとされていますが、可能ではあります。
あるModuleA
が別のModuleB
を利用するためには、まずはModuleB
が外部モジュールに公開したいProvider
をexportします(例ではBService
をexportします)
// b.module.ts
@Module({
providers: [BService, BPrivateService],
exports: [BService]
})
export class ModuleB {}
次に、ModuleA
がModuleB
を以下のようにimportします。
// a.module.ts
@Module({
providers: [AService],
exports: [AService]
})
export class ModuleA {}
あとはModuleA
内のController
やProvider
などで、同モジュールないのProvider
を利用するように外部モジュールであるBService
を利用することができます。
// a.service.ts
@Injectable()
export class AService {
constructor(private bService: BService){}
// ...
}
注意すべき点として、ModuleA
がModuleB
をimportしたからといって、ModuleB
の全てのProvider(例で言うとBPrivateService
)が使えるようになっていない点が挙げられます
まとめ・雑感
- Provider
- NestJSの最小単位の構成要素
- NestJSのDIコンテナーにて管理される
- interfaceを活用することによりテスト容易性が上がる(別の記事で説明したい…)
- Module
- 特定の関連するProviderやControllerなどを集約した単位
- 外部へのモジュールに公開するProviderを指定できる
- 逆に外部のモジュールを取り込み、そのProviderを利用することができる
- アプリケーションには必ずRootのModuleがある
- 思ったこと
- exportがあるために、pythonと違い外部に公開するクラスを制限できるのは良い
- DIコンテナという概念が途中でゲシュタルト崩壊したので調べてみたらさらにわからなくなった
-
- 個人的にはFactory関数との本質的な違いはなく、万能Factoryだと思っている
- ガチの人怖い…
-
- フレームワークの思想をSpringBootに寄せに来ている印象を強く受けた
- ExceptionFilterとかModuleとかRepositoryの紹介ページとか