【TSyringe】TypeScriptでDIコンテナを試す

## はじめに

最近お仕事で「DI」という概念に初めて触れたので、勉強を兼ねて [TSyringe](https://github.com/microsoft/tsyringe) を触ってみることにしました。この記事はその備忘録的なナニカです。

※注意：DI素人がふざけた文体で書いています。まじめな記事が読みたい方はブラウザバック推奨です。

## 本編

### そもそもDIってなんぞや

調べてみたところ、DIは `Dependency Injection` の略で、直訳すると「依存性の注入」です。

「依存性の注入」・・・？？？
依存性が何を指すのか、注入とは何を何にぶちこむことなのか、いまいちピンときませんよね？
私はお酒が好きなので、バーテンダーを例に説明します。

![job_bartender.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/d33b1d08-781b-e660-d503-fa745154de11.png)


バーテンダーがカクテルを作るためにはシェイカーが無いといけませんよね。
これは「バーテンダーがシェイカーに依存している」と言えるのでは無いでしょうか。これが「依存性」です。
これをclassを使って表現するとこんな感じになるでしょうか。

![スクリーンショット 0005-08-27 14.34.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/e46f59b6-8f1c-5357-58ff-81fa1ad7a217.png)


```ts
class Shaker {
  public shake() {
    return 'ｼｬｶｼｬｶ';
  }
}

class Bartender {
  public makeCocktail() {
    // 依存性
    const shaker = new Shaker();
    return shaker.shake();
  }
}
```

今は `Bartender` の中で `Shaker` をインスタンス化しちゃっているわけですが、これだと `Bartender` のテストを書くときに `Shaker` にも依存してしまって具合が悪いです。モックアップするのも面倒ですしね。
なら、`Bartender` をインスタンス化する際に `Shaker` インスタンスを外から渡すようにすればいいんじゃない？

```ts
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を導入していきます。とりあえず [公式ドキュメント](https://github.com/microsoft/tsyringe#installation) の言う通りにします。

パッケージをインストールして

```
$ npm i tsyringe reflect-metadata
```

デコレータとメタデータ使えるようにして

```tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
```

`reflect-metadata` を `TSyringe` 使う前にインポートしときます

```ts
import 'reflect-metadata';
```

~~めんどいんで~~ `reflect-metadata` には詳しく触れませんが、DIコンテナを実装するためにはクラスやメソッドが持つ情報を取得する必要があって、それに必要なAPIが入っているようです。

準備が出来たので、コードを書き直していきます。

```ts
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コンテナの力です。

### 試しにテストを書いてみる

```test.ts
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](https://stackblitz.com/edit/typescript-dqbk2c?file=index.ts) に貼っておきました。

今回は以下の記事を参考にさせていただきました。
- [TypeScriptのDIとTsyringeについて](https://zenn.dev/chida/articles/1f7df8f2beb6b6)
- [TSyringe で DI Container と Singleton](https://zenn.dev/optimisuke/articles/82b4f9bb1dfcc6)