NestJSでモジュールの循環参照を生じさせないためのモジュールレイヤードアーキテクチャとルール
NestJSを1年半ぐらい運用していていい感じのディレクトリ構成を思いついたので共有します。NestJSを使っているといつかモジュール同士の循環参照に悩むときがくるが、そのときに参考にしてくれればと思います。
一般的にレイヤードアーキテクチャというとcontroller
→service
→repository
のような依存関係を指すと思います。各レイヤーでは下方向のレイヤーしか依存してはいけず、同じレイヤー内部の依存や上方向の依存は禁止されています。レイヤーをスキップして依存していいかどうかは、チームのルールによります。
こうすることで、依存を一方向にすることができ、循環参照を防ぐとともに、レイヤー間の抽象化が進んで、置換や再利用がしやすくなります。
NestJSにおけるモジュールの循環参照
ただ、このレイヤードアーキテクチャのルールだけだと、NestJSのモジュールによるディレクトリ構成では循環参照がおきることがあります。
NestJSではcontroller
とservice
とrepository
間の依存だけでなく、module
同士の依存関係も考慮に入れる必要があります。
例えばusers
モジュールとtasks
モジュールがあったとします。users
モジュールでtasks
モジュール内部のservice
やrepository
を使いたいとき、users
→tasks
への依存が生じます。また、tasks
モジュールでusers
モジュール内部のservice
やrepository
を使いたいとき、tasks
→users
への依存が生じます。最初は片方しかありえないと思っていても、開発を進めていくと逆の依存が必要なユースケースが出てきたりします。
root/
┗ src
┠ users
┃ ┠ users.controller.ts
┃ ┠ users.module.ts
┃ ┠ users.repository.prisma.ts
┃ ┠ users.repository.inerface.ts
┃ ┗ users.service.ts
┗ tasks
┠ tasks.controller.ts
┠ tasks.module.ts
┠ tasks.repository.prisma.ts
┠ tasks.repository.inerface.ts
┗ tasks.service.ts
この循環参照は、モジュールのアーキテクチャ自体がレイヤードになっていないから起きます。
このような場合は、循環参照を避けるために、自分自身しか参照しない「コア」と、他に依存する「複合」にモジュールを分割したほうが良さそうです。今回だと
コア
- users
- tasks
複合
- user-tasks
とかでしょうか?
root/
┗ src
┠ users
┃ ┠ users.controller.ts
┃ ┠ users.module.ts
┃ ┠ users.repository.prisma.ts
┃ ┠ users.repository.inerface.ts
┃ ┗ users.service.ts
┗ tasks
┃ ┠ tasks.controller.ts
┃ ┠ tasks.module.ts
┃ ┠ tasks.repository.prisma.ts
┃ ┠ tasks.repository.inerface.ts
┃ ┗ tasks.service.ts
┗ user-tasks
┠ user-tasks.controller.ts
┠ user-tasks.module.ts
┗ user-tasks.service.ts
コードベースの大規模化によるserviceの再利用性の高まり
アプリが大きくなっていくと、1つのDBテーブルに対して複数の抽象化モデルがでてくるときがあります。例えば通常のユーザーという概念の他にスーパーユーザーという概念がでてくるかもしれません。素直にDBのテーブルをidを共通にして分けるのがベストだとは思いますが、既存のusers.repository.ts
やusers.service.ts
のロジックを再利用したいです。そんなとき、次のようなディレクトリ構成になります。
super-users.service.ts
では、users.service.ts
に依存しているとします。
root/
┗ src
┠ users
┃ ┠ users.controller.ts
┃ ┠ users.module.ts
┃ ┠ users.repository.prisma.ts
┃ ┠ users.repository.inerface.ts
┃ ┗ users.service.ts
┠ super-users
┃ ┠ super-users.controller.ts
┃ ┠ super-users.module.ts
┃ ┗ super-users.service.ts
┗ users.index.module.ts
上記2つの課題を改善するディレクトリ構成
なるべくNestJSの良さを引き出すために、ドメインごとにフォルダ分けをしています。
ここらへんはこの記事を参照してください。
また、ほかのservice
を依存するserivce
に特別な名前としてscenario
をつけて区別しています。
root/
┠ src
┃ ┠ domains
┃ ┃ ┠ users
┃ ┃ ┃ ┠ core
┃ ┃ ┃ ┃ ┠ users.controller.ts
┃ ┃ ┃ ┃ ┠ users.module.ts
┃ ┃ ┃ ┃ ┠ users.repository.prisma.ts
┃ ┃ ┃ ┃ ┠ users.repository.inerface.ts
┃ ┃ ┃ ┃ ┗ users.service.ts
┃ ┃ ┃ ┠ super-users
┃ ┃ ┃ ┃ ┠ super-users.controller.ts
┃ ┃ ┃ ┃ ┠ super-users.module.ts
┃ ┃ ┃ ┃ ┗ super-users.scenario.ts
┃ ┃ ┃ ┗ users.index.module.ts
┃ ┃ ┠ tasks
┃ ┃ ┃ ┗ core
┃ ┃ ┃ ┠ tasks.controller.ts
┃ ┃ ┃ ┠ tasks.module.ts
┃ ┃ ┃ ┠ tasks.repository.prisma.ts
┃ ┃ ┃ ┠ tasks.repository.inerface.ts
┃ ┃ ┃ ┗ tasks.service.ts
┃ ┃ ┗ user-tasks
┃ ┃ ┗ core
┃ ┃ ┠ user-tasks.controller.ts
┃ ┃ ┠ user-tasks.module.ts
┃ ┃ ┗ user-tasks.scenario.ts
┃ ┠ shares
┃ ┃ ┠ typeUtil.ts
┃ ┃ ┗ i18n.ts
┃ ┠ vendors
┃ ┃ ┠ prisma
┃ ┃ ┃ ┠ prisma.module.ts
┃ ┃ ┃ ┗ prisma.service.ts
┃ ┃ ┗ redis
┃ ┃ ┠ redis.module.ts
┃ ┃ ┗ redis.service.ts
┃ ┠ app.controller.ts
┃ ┠ app.module.ts
┃ ┗ main.ts
┗ 設定やドキュメント系
フォルダ/ファイル | 役割 |
---|---|
src | コードと設定系を分割するフォルダ |
src/domains | ドメイン特有のコードを入れるフォルダ |
src/vendors | prismaなどの依存注入したい外部ライブラリをいれるフォルダ |
src/shares | すべてのコードが依存する可能性がある便利系のコードを入れるフォルダ |
src/app.controller.ts | ヘルスチェック系などのコントローラー |
src/app.module.ts | すべてのモジュールに依存したモジュール |
src/main.ts | エントリーポイント |
依存のイメージマップ
ルール
レイヤードアーキテクチャに準じたルールを決めます。箇条書きにすると多く見えますが、基本的には
- 同じレイヤーには依存できない
- 下方向にレイヤーにのみ依存できる
- 上方向のレイヤーには依存できない
を具体的に言っただけです。
- shareフォルダにはユーティリティ系のファイルを置く
- shareフォルダ内のファイルはDIで置き換えることは少ないと想定して、直
import
する。つまりモジュール化しない。 - vendorsフォルダにはprismaなどのDIで置き換える可能性のある外部ライブラリをモジュール化して置く。
- vendorsフォルダ内のモジュールは他のモジュールに依存してはいけない(ただし、shareフォルダ内のユーティリティ系ファイルは依存OK)。
- domainsフォルダにこのドメイン特有のモジュールを置く。
- domainsファルダ内のモジュールはvendorsフォルダ内部のモジュールに依存しても良い。
- domainsフォルダ内の各モジュールは、
service
ファイルかscenario
ファイルのどちらか1つのみしか置いてはいけない。service
ファイルとscenario
ファイルの両方を置いてはいけない。 -
service
ファイルのあるモジュールは、他のdomainsフォルダ内のモジュールに依存してはいけない。つまりservice
ファイルは基本的に同一フォルダ内のrepository
にしか依存できない。 -
scenario
ファイルのあるモジュールは、他のdomainsフォルダのservice
ファイルがあるモジュールに依存してもよい。 -
scenario
ファイルのあるモジュールは、他のdomainsフォルダのscenario
ファイルがあるモジュールに依存してはいけない。
もし、scenario
に依存したいさらなるscenario
がユースケースとして発生したら、さらに新しいレイヤーを作らないといけないかもですね...
Discussion