前書き
以前、【TypeScript】非情報系卒の駆け出しエンジニアなりにDIするコードをvanillaで書いてみたという記事を公開させていただいたのですが、その記事内で示したコードでは解決できないことの方が多く、それならstatic classで良くね?ってコードでした。
公開後もシコシコとDIについてほかのコードを読んだりしており、現段階まででいったん一区切りつけられるだけのDIをする実装ができたので進捗報告という形で本記事を執筆しました。
とはいえ車輪の再発明(むしろ劣化パチモン)であることには変わりないので、InversifyJSやtsyringeを使った方がいいのは自明です。
いつも通り稚拙な文章ですがお付き合いください。よろしくお願いします。
前提
- reflect-metadataを使わない(限界を試したいので)
- デコレータを使うため、TSconfigの書き換えは必要
- 以下のコードはすべて同じファイル内に存在する
実装
先にインスタンスを保持するMapとデコレータを定義します。
/** 依存性注入クラスの型 */
interface Type<T> extends Function {
new (...args: any[]): T;
}
/** DIマッピング用 */
const DIMapper = new Map<string, Object>();
/** クラスにつけるデコレータ */
function mapping(_constructor: Type<any>) {
DIMapper.set(_constructor.name, new _constructor())
}
/** プロパティに依存性を注入するデコレータ */
function inject (arg: string) {
return (target: any, member: string): any => {
return {
configurable: true,
enumerable: true,
value: DIMapper.get(arg),
writable: true
};
};
}
型 interface Type<T> extends Function
については前の記事で微妙に説明しているので見てください。
各デコレータについては
- もう怖くないTypeScriptのDecorator機能
- TypeScriptによるデコレータの基礎と実践
- TypeScriptのDecoratorまとめ
- TypeScriptのDecoratorメモ
を読むのがいいかと思います。
参考になりました。ありがとうございます。
この中のプロパティデコレータはやや面倒で、PropertyDescriptor型を返す必要があります。この型は Object.defineProperty()で見るアレです。
また、返り値はanyかvoidである必要があるためanyを返却しています。strictモードで開発していたら注意を受けました。
classにつけるデコレータ mapping
は、 .name
でクラス名を取得しそれに対応つける形でインスタンスを持たせています。そのため、
/** DIマッピング用 */
const DIMapper = new Map<string, Object>();
という型になっています。
与える引数が文字列のクラス名なところがDjango ORMのprefetchみたいですね。
使ってみる
class A, B, Cをそれぞれ定義しました。入れ子になっていて注入が面倒そうです。
@mapping
class A {
prop: string;
constructor() {
this.prop = 'propA';
}
}
@mapping
class B {
@inject('A') private aaa!: A;
constructor() {}
get ap(): A {
return this.aaa;
}
}
@mapping
class C {
@inject('B') private bbb!: B;
constructor() {}
get bp(): B {
return this.bbb;
}
}
出来ているかを見てみます。
console.log(Array.from(DIMapper.entries())); // [ [ 'A', A { prop: 'propA' } ], [ 'B', B {} ], [ 'C', C {} ] ]
インスタンスがマッピングされています。ですがデコレータをつけているプロパティが入っていません。
大丈夫なのでしょうか?
……大丈夫です。インスタンス生成後にはちゃんとDIされています。 いえ、正確にはconstructor処理中では依存性が注入されるインスタンスにアクセスできないので大丈夫ではないのですが、ここはいったん無視します。
追記:確認したところ、constructor内でも依存性注入済みのプロパティ値にアクセスできました。
確認してみましょう。
const ccc = DIMapper.get('C')! as C;
console.log(ccc.bp); // B {}
console.log(ccc.bp.ap); // A { prop: 'propA' }
いろいろ見なかったことにすれば出来ていそうですね!
終わりに
進捗報告終わりです。前の記事から少しは成長できているでしょうか?
上記のコードの問題点として、クラスの定義順次第ではインスタンスがMap上に格納されていなくて危険だったり、プロパティデコレータに与える文字列のタイポの危険性、Non-null assertion operatorをつける必要があったりと課題がたくさんあります。。
また今後の展望として、reflect-metadataを用いるか、よくわかっていないのですが引数のデコレータを用いるなどが考えられます。
今後もゆるくDI周りを漁っていきます。
読んでいただきありがとうございました。