LoginSignup
20
11

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

Posted at

はじめに

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

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

本編

そもそもDIってなんぞや

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

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

job_bartender.png

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

スクリーンショット 0005-08-27 14.34.20.png

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

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

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

reflect-metadataTSyringe 使う前にインポートしときます

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

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

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 に貼っておきました。

今回は以下の記事を参考にさせていただきました。

20
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
11