👻

TSyringe で DI Container と Singleton

2022/10/16に公開

はじめに

TypeScript で DI Container 作りたくなったので、調査して試してみた。
前回書いた記事(TypeScript で依存性逆転の原則と DI とテスト)では、DI Container について書かなかったので、こっちで補完出来たらいいかなと思う。

まず、DI Container の概念については、DI・DI コンテナ、ちゃんと理解出来てる・・? - Qiitaとか、ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本(成瀬 允宣)|翔泳社の本を読んでもらったら良いかと思う。
自分の理解も書いておくと、ただの DI(特に Constructor Injection)だと、コードのトップで new しまくらないといけないのが微妙だから、依存関係はできるだけメインの処理とは別のところに設定ファイルっぽくコンテナ(Docker とは無関係)としてまとめちゃいなよって概念なのかなと思ってる。(指摘あればウェルカムです!)

TypeScript で DI Container する方法を調べると、InversifyJSTSyringeが有名みたい。
自分で作ろうと思うと、以下の記事を参考にしたらよさそう。ただ、TypeScript は JavaScript にトランスパイルするって制約があるため、TypeScript レベルを上げてからでないと難しそう。
DI/DI コンテナとは?TypeScript での軽量 DI 実装まで - Qiita

シンプルにやりたかったので、まずはTSyringeを試してみた。
いい感じの情報ないかなと探してると、以下の記事がやってることシンプルでわかりやすかった。
TypeScript の DI と Tsyringe について

ただ、トップのファイルで register してるから、それって new してるのとほとんどおんなじやんってなった。
なので、いい感じのファイル構成で試してる人いないか探してみた。すると下記サイトを見つけたので、参考にした。
Using tsyringe for dependency injection without using the class syntax - DEV Community 👩‍💻👨‍💻

他にも調べてると、TSyringe とか InversifyJS を使うと、楽にシングルトン作れることも分かった。
この記事が超絶わかりやすかった。InversifyJS で書かれてたので TSyringe でも試してみることにした。
Node で書く時 TypeScript で DI したい | CYOKODOG

以下では、下記 2 点を書く。
TSyringe とか DI Container を使おうとしている人向けに、上記参照先の補完情報を提供できればと思う。

  1. TSyringe で DI Container
  2. TSyringe でシングルトン

1. TSyringe で DI Container

はじめにでも書いたけど、下記 2 つのサイトを参考にして実際に TSyringe を動かしてみた。
TypeScript の DI と Tsyringe について
Using tsyringe for dependency injection without using the class syntax - DEV Community 👩‍💻👨‍💻

コードは、optimisuke/hello-dicontainer-typescriptに置いてる。

まず、トップは、こんな感じ。
DI Container を使わかったら、new User(new Database)とすべき部分が、container.resolve(User)になってる。
中で使ってる、Databaseクラスは、di.tsで register してる。

index.ts
import 'reflect-metadata'
import { container } from './di'
import User from './user'

export const user = container.resolve(User)

user.userId = 1
user.userName = 'yamada'
user.saveUser()

中で使うクラスの登録は、di.tsに移した。登録した container を再度 export してる。

di.ts
import { container } from 'tsyringe';
import Database from './database'

export { container };

container.register('IDatabase', {
    useClass: Database
})

あと、中身の User と Database はこんな感じ。
デコレーターで指定することで、inject したりされたりできるようにする。

user.ts
import { injectable, inject } from 'tsyringe'

export interface IDatabase {
    saveUser: (user: User) => void
}

@injectable()
export default class User {
    userId: number = 0
    userName: string = ''

    constructor(
        @inject('IDatabase')
        private database: IDatabase
    ) { }

    saveUser() {
        if (this.userId) {
            this.database.saveUser(this)
        }
    }
}
database.ts
import User, { IDatabase } from "./user";

export default class Database implements IDatabase {
  saveUser(user: User) {
    console.log(`Saved ${user.userName}!`); // Saved yamada!
  }
}

設定はこんな感じ。

package.json
{
  "name": "hello-dicontainer-typescript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "npx ts-node ./index.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "reflect-metadata": "^0.1.13",
    "tsyringe": "^4.7.0"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

以上、ほとんどコピペで恐縮やけど、register 部分を分割してる。
依存関係は事前に register で登録しておいて、使うときに resolve でインスタンスを作ってもらう感じで実装していくイメージ。

2. TSyringe でシングルトン

はじめにでも書いたが、以下の記事がすごくわかりやすかったので、TSyringe で試してみた。
Node で書く時 TypeScript で DI したい | CYOKODOG

register する部分が雰囲気違うけど、デコレーター使う部分とかはほぼほぼ一緒。

コードはoptimisuke/hello-ts-tsyringe-singletonに置いてる。

まず、KatanaクラスとSamuraiクラスはこんな感じ。
シンプルにするためか、インターフェースは使ってない。
(Samurai が Katana に依存しちゃってる(依存性逆転の原則ができてない)けど、Samurai は Katana を大事にしてるやろし、それはそれでいいんじゃないかという気もする。知らんけど。)

service.ts
import { injectable, inject, singleton } from "tsyringe";

@injectable()
export class Katana {
    sound = "ブンッ!";

    swing() {
        return this.sound;
    }
}

// @injectable()
@singleton()
export class Samurai {
    constructor(
        @inject('Katana')
        private katana: Katana
    ) { }

    fight() {
        return this.katana.swing();
    }

    pwoerUp() {
        this.katana.sound = "ブウウウウンッ!!";
    }
}

メインのindex.tsではKatanaを register して、samuraiを resolve してる。
resolve で、2 回、Samuraiインスタンスを取得してるけど、@singleton()をつけてるので、同じものになる。
なので、片方をpowerUp()するともう片方もpowerUp()してて期待通り。

index.ts
import "reflect-metadata";
import { container, injectable } from "tsyringe";
import { Katana } from "./service";
import { Samurai } from "./service";


container.register('Katana', {
    useClass: Katana
})

const samuraiA = container.resolve(Samurai)
const samuraiB = container.resolve(Samurai)

console.log(samuraiA.fight()); // ブンッ!
samuraiA.pwoerUp();
console.log(samuraiA.fight()); // ブウウウウンッ!!
console.log(samuraiB.fight()); // ブウウウウンッ!! (シングルトンのとき)

ついでに、設定はこんな感じ。

package.json
{
  "name": "hello-ts-tsyringe-singleton",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "npx ts-node ./index.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "reflect-metadata": "^0.1.13",
    "tsyringe": "^4.7.0"
  }
}

おわりに

DI Container 食わず嫌いな感じだったけど、自分で動かしながら動作見てたら、だいぶ理解が進んだ。
そのうち、もうちょい、本格的に使ってみたい。

GitHubで編集を提案

Discussion