NestJSでInjectされるオブジェクトの初期化のタイミング
実はここのところ NestJS を触っていました。これについての感想などはまたいずれ書くとして、DI コンテナそのものに不慣れだったので NestJS の DI でとても悩みましたという話を少々。(例によって分かってしまえばどうということはないんだけど)
※ 一応間違いないはずだけど、思い違いがあれば教えてください。
以下に(module定義を除いて)DI コンテナに登録する provider とそれを利用するコードを挙げる。仮に以下のようなコードだったとして、実際にどの部分のコードがどの順番で実行されるのかを確認し、それによって生じる制限と、その制限を克服する方法を整理する。
コード例
NestJS は TypeScript で動いてます。
@Injectable()
export class Dependee {
constructor () {
(1)
}
}
import { Dependee } from '..'
export class Depender {
constructor (
dependee: Dependee
) {
(2)
}
}
- Depender が Dependee に依存している
- Depender の constructor で Dependee が inject される
実行順
- Dependee の constructor が実行され(default の provider の挙動)て、インスタンスが生成される
- 生成されたインスタンスが Depender の constructor で inject される
できないこと
常に Dependee が先にインスタンス化されるので、Depender の constructor で取得できる値を Dependee の constructor に渡す方法がない。
具体的にどう困るのか
例えば Controller で @Query や @Param で取得した値から「特定の値を持つ provider のインスタンスを生成する」ことは組み込みの DI の機能では実現できない。これは Controller の constructor が実行されるタイミングですでに Dependee のインスタンスは生成されていて、Action に該当するメソッドが呼ばれるタイミングは二周遅れになっているため。
ではどうするとよいのか。
Dependeeに値を渡す方法
どうにかして値を渡したい場合、どうするのがよいのか。方法としては大きく分けて2つ、全部で4つくらいありそう。
- 最初にあり得る値をセットするインスタンス生成法を全部 DI コンテナに登録しておいて必要なものを必要な人が取得する
- Inject の機構を利用せずに Depender の constructor の中で手動で Dependee をインスタンス化する
- DI になっていない1
- 実行する処理そのものが書かれているメソッドに渡す
- property ではなくメソッドのシグニチャで class の特徴を表すことになる
- 複数のオブジェクトにそれらを渡しながら処理していく場合にインターフェイスの変更が高コストになるのでカッチリ設計が固まっている場合以外は工夫が必要。例えば context オブジェクトを導入するにしても今回の constructor の実行順の問題は同様に残る。
- constructor に渡すのは諦めてsetter を用意して setter で値を渡す
- この場合、Dependee の該当 property は readonly にはできなくなる
- 言語の機能で immutable にはできないので、どうしてもこだわるなら何らかの工夫が必要
どうしてもインスタンスの初期値を最初に決めたのちは途中で挙動が変わってほしくない場合
1 は DI コンテナの機能も素直に活かしつつ、初期値で挙動を固定して途中で変わってほしくないという要望も完璧に満たすことができる。具体的には NestJS では provider 定義に useFactory を使うとこうしたことが可能となる。
Custom providers | NestJS - A progressive Node.js framework
ただし、例えば日付やお金など値の範囲が無限になってしまうものに対してはこの方法は使えない。曜日くらいなら可能。
2 は Dependee の挙動をインスタンス生成時に固定するという目的に対して最も解決が早く、新たな学習コストがない。ただし、用意された DI の機能は使っていないのでちゃんと依存関係の管理ができているかというとあやしくなってくる。
インスタンスの挙動が変わることを許容できるなら
いちばん素朴で素直なのは 3 かな。インスタンスそのものが何をするのか知っているという形ではなくなってしまうけど、その部分の責務をすべてメソッドに担わせる形。
副作用として interface でちゃんと設計を練ってあげてそれを type として利用するようにしておくと class そのものが変わっても耐えられる。こうなると本当に DI っぽい。
4 は 3 と似てるけど setter があるとメソッドの呼び出し順に依存するので避けられるなら避けた方がよさそう。setter で丸ごと放り込むのが雑にやるには早いのは早いけど。
とは言え DI コンテナを使えば DI なのかと言われるとやや疑問は残る。依存先のオブジェクトの生成方法は隠蔽されているが、interface ではなく provider の実装そのものを type として指定している場合に、どこまで DI と呼んでよいのか…? いずれにせよ絶対にコンテナに乗っていないといけない、あるいは乗っていればよいと考えるのも何か違う気がする。ただし、NestJS の場合は DI コンテナに依存するために module 定義に乗せておくと compodoc https://compodoc.app/ というツールで依存関係を visualize できるので、これは大きい。また、どこで何を利用しているのかが実行前に分かればリファクタリングは行いやすい。 ↩