はじめに
最近お仕事で「DI」という概念に初めて触れたので、勉強を兼ねて TSyringe を触ってみることにしました。この記事はその備忘録的なナニカです。
※注意:DI素人がふざけた文体で書いています。まじめな記事が読みたい方はブラウザバック推奨です。
本編
そもそもDIってなんぞや
調べてみたところ、DIは Dependency Injection
の略で、直訳すると「依存性の注入」です。
「依存性の注入」・・・???
依存性が何を指すのか、注入とは何を何にぶちこむことなのか、いまいちピンときませんよね?
私はお酒が好きなので、バーテンダーを例に説明します。
バーテンダーがカクテルを作るためにはシェイカーが無いといけませんよね。
これは「バーテンダーがシェイカーに依存している」と言えるのでは無いでしょうか。これが「依存性」です。
これをclassを使って表現するとこんな感じになるでしょうか。
class Shaker {
public shake() {
return 'シャカシャカ';
}
}
class Bartender {
public makeCocktail() {
// 依存性
const shaker = new Shaker();
return shaker.shake();
}
}
今は Bartender
の中で Shaker
をインスタンス化しちゃっているわけですが、これだと Bartender
のテストを書くときに Shaker
にも依存してしまって具合が悪いです。モックアップするのも面倒ですしね。
なら、Bartender
をインスタンス化する際に Shaker
インスタンスを外から渡すようにすればいいんじゃない?
class Bartender {
constructor(private shaker: Shaker) {}
public makeCocktail() {
// const shaker = new Shaker();
return this.shaker.shake();
}
}
const bartender = new Bartender(new Shaker());
これが「依存性の注入」です。え、・・・こんだけ長々書いておいてこれだけ・・・?
すみません、これだけです。これがDIの考え方になります。
DIコンテナとは?
今回触ってみようとしている TSyringe
はTypeScriptで「DIコンテナ」を実現するためのパッケージです。
疑問に思った方もいるかもしれませんが、先程の章で書いた通り、使いたいインスタンスをコンストラクタで渡すだけでDIは実現可能です。では、なぜ「DIコンテナ」なるものが必要なのでしょうか?
答えはシンプルで「 new
しまくるの面倒くね?」です。
先程の例ではあまり実感できないと思いますが、実際の開発ではもっと多くのclassが組み合わさっていくことを考えると、ちょっとゾッとしますよね。きっと new
がゲシュタルト崩壊することでしょう。
それをうまいこと解決するツールが「DIコンテナ」になります。
TSyringeで書き直してみよう
早速、TSyringeを導入していきます。とりあえず 公式ドキュメント の言う通りにします。
パッケージをインストールして
$ npm i tsyringe reflect-metadata
デコレータとメタデータ使えるようにして
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
reflect-metadata
を TSyringe
使う前にインポートしときます
import 'reflect-metadata';
めんどいんで reflect-metadata
には詳しく触れませんが、DIコンテナを実装するためにはクラスやメソッドが持つ情報を取得する必要があって、それに必要なAPIが入っているようです。
準備が出来たので、コードを書き直していきます。
import 'reflect-metadata';
import { inject, injectable, container } from 'tsyringe';
@injectable() // コレ付けとくとinjectできるようになるっぽい
class Shaker {
public shake() {
return 'シャカシャカ';
}
}
@injectable()
class Bartender {
// 'Shaker' ってキーでinjectしてねって宣言
constructor(@inject('Shaker') private shaker: Shaker) {}
public makeCocktail() {
return this.shaker.shake();
}
}
const bartender = container
.register('Shaker', { useClass: Shaker }) // 必要なclassをコンテナに登録する
.resolve(Bartender); // 登録したclassをinjectしつつインスタンス化する
console.log(bartender.makeCocktail()); // シャカシャカ
はい。「シャカシャカ」をコンソールに吐き出すだけの無意味なコードができましたね。カレーを食べてる途中で口を拭くことくらい無意味です(どうせ次の一口で汚れるのについついやっちゃう)
冗談はさておき、どこでも new
してないですね。これがDIコンテナの力です。
試しにテストを書いてみる
import { Bartender, Shaker } from '.';
// jestとかvitestとか入れたかったけどめんどくさかったので簡易版で
const assert = <T>(name: string, target: T, expected: T) => {
const result = target === expected ? 'success' : 'failed';
console.log(`${name} : ${result}`);
};
class MockedShaker implements Shaker {
shake() {
return 'test';
}
}
const bartender = container
// Shakerはモックされているので依存なくテストできる
.register('Shaker', { useClass: MockedShaker })
.resolve(Bartender);
// makeCocktail : success
assert('makeCocktail', bartender.makeCocktail(), 'test');
Shaker
classに依存しないテストが書けていますね。jestなんかでモックアップするよりシンプルで良きです。
まとめ
- DIとは、使いたいインスタンスをコンストラクタで渡すようにすればテストとかしやすくなっていいよねってやつ
- DIコンテナは、
new
しまくらないといけない問題をよしなに解決できるやつ - バーテンダーさんは何食わぬ顔でシェイクしてますが、アレめっちゃ手が冷くて痛いです(経験談)
さいごに
正直最初、なんかよくわからんデコレータがついててキモいなって思ってましたが、やってることも考え方もとてもシンプルないいヤツでした。
オブジェクト指向も素人同然なので、SOLID原則とか抽象・具象とかは全く分かりません・・・お許しください!
一応コードは StackBlitz に貼っておきました。
今回は以下の記事を参考にさせていただきました。