InversifyJSを使ったTypeScriptでのDI実装 🪄

## はじめに

[前回](https://qiita.com/nao-United92/items/b53e51a9f6e03ecef638)、DI・DIコンテナについて深掘りしました。

ここでは更に実務イメージを深めるために、**[InversifyJS](https://github.com/inversify/InversifyJS) を使ったTypecriptでのDI実装**にフォーカスしたサンプルコードをご紹介します。

* InversifyJS
TypeScript製のDIコンテナを提供するライブラリ、コード全体で4KBと軽量



## 目次

1. [軽くおさらい](#-軽くおさらい)
2. [事前準備](#-事前準備)
3. [構成](#-構成)
4. [コードの実装](#-コードの実装)
5. [参考文献](#-参考文献)

## 🪄 軽くおさらい
### DI
* Dependency Injection（依存性注入） の略。
* あるクラスや関数が必要とするオブジェクト（＝依存するもの）を、自分で作らずに外部から「注入」してもらう設計パターン

### DIコンテナ
* Dependency Injection Container（依存性注入コンテナ） の略。
* オブジェクトの依存関係をコードの中で直接 new する代わりに、外部から注入するパターンを管理する仕組み。

### デコレーター
* クラスやそのプロパティ・メソッドなどに特別な意味や振る舞いを付与する構文
* TypeScript（や Python など）にある機能で、`@` で始まる構文が特徴

#### `@Injectable`
* DIコンテナにクラスを依存対象として認識させるためのデコレーター
* 主に TypeScript や JavaScript（ES6+） で DI ライブラリ（例：InversifyJS）と組み合わせて使われる。
👉 **クラスをDIコンテナに登録可能にする**ための宣言

#### `@Inject`
* コンストラクタ引数に、特定の依存を注入してほしいことを明示するデコレーター
👉 **その引数に注入する依存を明示的に指定する**ための宣言

## 🪄 事前準備
### 1. プロジェクトディレクトリの作成と移動
まず、プロジェクト用のディレクトリを作成し、その中に移動
```bash
mkdir cafe-di-example
cd cafe-di-example
```
### 2. package.json の初期化
プロジェクト情報を設定する`package.json` を作成
```bash
npm init -y
```
### 3. 必要なライブラリのインストール
DIとテストに必要なライブラリをインストール
```bash
npm install inversify reflect-metadata
npm install --save-dev jest @types/jest ts-jest typescript
```
* inversify：DIコンテナ本体
* reflect-metadata：InversifyJSがデコレーターを使用するために必要
* jest, @types/jest, ts-jest：JestテストフレームワークとTypeScriptでJestを使うための設定
* typescript：TypeScriptコンパイラ

### 4. ソースディレクトリ・設定ファイルの作成
ソースコードを格納する `src` ディレクトリを作成
```bash
mkdir src
```
`tsconfig.json` と `jest.config.js` をプロジェクトルートに作成
InversifyJSのデコレーターと `reflect-metadata` を使用するために、`tsconfig.json` に以下を設定
```json: tsconfig.json 
{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "lib": ["es2018"],
    "experimentalDecorators": true, // これが必要
    "emitDecoratorMetadata": true,   // これが必要
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "rootDir": "./src",             // ソースコードのルートディレクトリ
    "outDir": "./dist"              // コンパイル出力先ディレクトリ
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.test.ts" // テストファイルはコンパイル対象から外すことが多い
  ]
}
```
```js: jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'], // テストファイルの場所を指定
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
};
```

## 🪄 構成
#### 🧩 以下ファイルを作成(役割別に分割)
* interfaces.ts：インターフェースの定義
* types.ts：DIコンテナのバインディングに使用するシンボルの定義
* CafeRepository.ts：リポジトリの実装
* CafeService.ts：サービスの実装
* container.ts：DIコンテナの設定とエクスポート
* index.ts：DIコンテナを使ってインスタンスを取得し、使用する例
* CafeService.test.ts：Jestを使ったテストコード

```
your-project-root/
├── node_modules/         # インストールされたライブラリ
├── package.json          # プロジェクト情報と依存関係
├── tsconfig.json         # TypeScriptコンパイラ設定
├── jest.config.js        # Jest設定 (必要な場合)
├── src/                  # ソースコードディレクトリ
│   ├── interfaces.ts     
│   ├── types.ts          
│   ├── CafeRepository.ts 
│   ├── CafeService.ts    
│   ├── container.ts      
│   ├── index.ts          
│   └── CafeService.test.ts 
└── .gitignore            # Git管理から除外するファイル設定など
```

### 🧩 役割：モチーフはカフェ
#### ICafeService
「お客様対応（注文受付や情報提供など）」
👉 お店が提供するサービス内容(設計図)

#### CafeService
「実際にお客様対応をする窓口担当者」
👉 `ICafeService` を具体的に実行するクラス、依存する`CafeRepository`を使って処理を行う。

#### ICafeRepository
「カフェの情報そのものの検索・保管する」
👉 データの「取得」「保存」といったデータ操作内容(設計図)

#### CafeRepository
「実際にカフェ情報を管理し、資料を探し出す担当者」
👉 `ICafeRepository` という役割を具体的に実行するクラス
👉 データベース等と連携してデータ操作を行います（例ではダミーデータ）。

#### DI Container (container.get)
「必要な担当者（例: 窓口担当者）に、必要な役割を持つ別担当者（例: 資料保管係）を自動的に手配してくれる『運営管理者』」
👉　クラスのインスタンス生成と、そのクラスが必要とする他の依存オブジェクト（他のクラスのインスタンス）の注入（受け渡し）を管理してくれる仕組み。
👉　`container.get` は「運営管理者に頼んで、必要な担当者（依存解決済みの状態）をアサインしてもらう」イメージ。
#### 💡 まとめ
* インターフェースは **「役割」や「契約」**
* 具象クラスは **「役割を担う具体的な担当者やシステム」**
* DIコンテナは **「担当者間の連携や手配を管理する運営管理者」**

## 🪄 コードの実装
### interfaces.ts
```ts
// Cafe データの型定義 (簡易版)
export interface Cafe {
  id: string;
  name: string;
  address: string;
  // 他のプロパティ...
}

// CafeRepository インターフェース
export interface ICafeRepository {
  getCafe(id: string): Promise<Cafe | undefined>;
  // 他のメソッド...
}

// CafeService インターフェース
export interface ICafeService {
  findCafe(id: string): Promise<Cafe | undefined>;
  // 他のメソッド...
}
```

### types.ts
DIコンテナでクラスをバインディングする際に使用するシンボルを定義。
文字列でも可能ですが、シンボルを使うことで名前の衝突を防ぎ、より安全に。
```ts
export const TYPES = {
  ICafeRepository: Symbol.for("ICafeRepository"),
  ICafeService: Symbol.for("ICafeService"),
};
```
### CafeRepository.ts
`ICafeRepository` インターフェースを実装するクラス。
データの取得を担当しますが、この例ではダミーデータを返します。
```ts
import "reflect-metadata"; // InversifyJSのデコレーターを使用するファイルではインポートが必要
import { injectable } from "inversify";
import { ICafeRepository, Cafe } from "./interfaces";

@injectable() // InversifyJSに管理されるクラスであることを示すデコレーター
export class CafeRepository implements ICafeRepository {
  async getCafe(id: string): Promise<Cafe | undefined> {
    console.log(`[CafeRepository] Fetching cafe with id: ${id}`);
    // 実際にはデータベースやAPIからデータを取得する処理
    if (id === "cafe-123") {
      return { id: "cafe-123", name: "サンプルカフェ", address: "東京都渋谷区..." };
    }
    if (id === "cafe-456") {
      return { id: "cafe-456", name: "美味しいコーヒー店", address: "大阪府中央区..." };
    }
    return undefined; // 見つからない場合
  }
}
```
### CafeService.ts
`ICafeService` インターフェースを実装するクラス、ビジネスロジックを記述。`ICafeRepository` に依存していますが、コンストラクタで受け取る（依存性を注入される）形になっています。
```ts
import "reflect-metadata"; // InversifyJSのデコレーターを使用するファイルではインポートが必要
import { injectable, inject } from "inversify";
import { ICafeService, ICafeRepository, Cafe } from "./interfaces";
import { TYPES } from "./types";

@injectable() // InversifyJSに管理されるクラスであることを示すデコレーター
export class CafeService implements ICafeService {
  private cafeRepository: ICafeRepository;

  // コンストラクタで依存オブジェクト (ICafeRepository) を受け取る
  // @inject デコレーターでDIコンテナにどの型のインスタンスを注入するか指示する
  constructor(
    @inject(TYPES.ICafeRepository) cafeRepository: ICafeRepository
  ) {
    this.cafeRepository = cafeRepository;
  }

  async findCafe(id: string): Promise<Cafe | undefined> {
    console.log(`[CafeService] Processing request for cafe id: ${id}`);
    // Repositoryを使ってデータを取得
    const cafe = await this.cafeRepository.getCafe(id);

    // ここにCafeに関する追加のビジネスロジックを記述可能
    // 例: データの整形、権限チェックなど

    return cafe;
  }
}
```
### container.ts
DIコンテナを生成し、インターフェースと実装クラスのバインディングを行います。
```ts: 
import "reflect-metadata"; // InversifyJSのContainerを使用するファイルではインポートが必要
import { Container } from "inversify";
import { TYPES } from "./types";
import { ICafeRepository, ICafeService } from "./interfaces";
import { CafeRepository } from "./CafeRepository";
import { CafeService } from "./CafeService";

const container = new Container();

// インターフェース (TYPES.ICafeRepository) が要求されたら、
// CafeRepository のインスタンスを提供するようにバインディング
container.bind<ICafeRepository>(TYPES.ICafeRepository).to(CafeRepository);

// インターフェース (TYPES.ICafeService) が要求されたら、
// CafeService のインスタンスを提供するようにバインディング
// CafeServiceはコンストラクタでICafeRepositoryを要求しているので、
// InversifyJSが自動的にICafeRepositoryのインスタンス(CafeRepository)を注入してくれる
container.bind<ICafeService>(TYPES.ICafeService).to(CafeService);

// コンテナをエクスポート
export { container };
```
### index.ts
DIコンテナを使ってサービスのインスタンスを取得し、使用する例です。
アプリケーションのエントリーポイントなどで使用します。
```ts: 
import { container } from "./container";
import { TYPES } from "./types";
import { ICafeService } from "./interfaces";

// DIコンテナからICafeServiceのインスタンスを取得
// コンテナがCafeServiceを生成し、その際に依存するCafeRepositoryも自動的に注入してくれる
const cafeService = container.get<ICafeService>(TYPES.ICafeService);

// サービスを使って処理を実行
async function run() {
  console.log("--- Getting cafe-123 ---");
  const cafe1 = await cafeService.findCafe("cafe-123");
  console.log("Result:", cafe1);

  console.log("\n--- Getting cafe-456 ---");
  const cafe2 = await cafeService.findCafe("cafe-456");
  console.log("Result:", cafe2);

  console.log("\n--- Getting non-existent-cafe ---");
  const cafe3 = await cafeService.findCafe("non-existent-cafe");
  console.log("Result:", cafe3);
}

run();
```

`index.ts` を `ts-node` などで実行すると、DIコンテナを通してインスタンスが生成され、依存性が解決されて処理が実行されることが確認できます。
```bash
npx ts-node src/index.ts
```
### CafeService.test.ts
Jestによる `CafeService` のテストコード
`CafeService` は `ICafeRepository` に依存していますが、テスト時にはモックの `ICafeRepository` を注入することで、`CafeService` 単体としてテスト可能。
```ts: 
import "reflect-metadata"; // InversifyJSデコレーターが使われているクラスをテストする場合に必要
import { CafeService } from "./CafeService";
import { ICafeRepository, Cafe } from "./interfaces";

describe("CafeService", () => {
  let cafeService: CafeService;
  // Jestのモック関数を持つICafeRepositoryの型を定義
  let mockCafeRepository: jest.Mocked<ICafeRepository>;

  // 各テストの前に実行される設定
  beforeEach(() => {
    // ICafeRepositoryのモックオブジェクトを作成
    mockCafeRepository = {
      getCafe: jest.fn(), // getCafeメソッドをモック関数として定義
      // ICafeRepositoryに他のメソッドがあればここに追加
    } as jest.Mocked<ICafeRepository>; // 型キャストでJestのモックメソッドであることを示す

    // CafeServiceのインスタンスを生成し、モックのCafeRepositoryを注入する
    cafeService = new CafeService(mockCafeRepository);
  });

  it("目的のカフェを見つけた", async () => {
    const cafeId = "test-cafe-id";
    const mockCafe: Cafe = { id: cafeId, name: "Test Cafe", address: "Test Address" };

    // モックのgetCafeメソッドが、指定されたIDで呼ばれたときに何を返すかを定義
    mockCafeRepository.getCafe.mockResolvedValue(mockCafe);

    // CafeServiceのメソッドを呼び出す
    const result = await cafeService.findCafe(cafeId);

    // 結果の検証 (アサーション)
    expect(result).toEqual(mockCafe); // 返り値が期待するモックデータであること
    expect(mockCafeRepository.getCafe).toHaveBeenCalledWith(cafeId); // モックのgetCafeが正しいIDで呼ばれたこと
    expect(mockCafeRepository.getCafe).toHaveBeenCalledTimes(1); // モックのgetCafeが1回呼ばれたこと
  });

  it("目的のカフェを見つけられなかった", async () => {
    const cafeId = "non-existent-id";

    // モックのgetCafeメソッドが、指定されたIDで呼ばれたときにundefinedを返すように設定
    mockCafeRepository.getCafe.mockResolvedValue(undefined);

    // CafeServiceのメソッドを呼び出す
    const result = await cafeService.findCafe(cafeId);

    // 結果の検証
    expect(result).toBeUndefined(); // 返り値がundefinedであること
    expect(mockCafeRepository.getCafe).toHaveBeenCalledWith(cafeId); // モックのgetCafeが正しいIDで呼ばれたこと
    expect(mockCafeRepository.getCafe).toHaveBeenCalledTimes(1); // モックのgetCafeが1回呼ばれたこと
  });

  // CafeServiceのfindCafeメソッド内の他のビジネスロジックに関するテストも追加可能
});
```

#### ⚡️ テストを実行
```
npm test
# または npx jest
```



## 💫 参考文献
* [InversifyJS](https://github.com/inversify/InversifyJS)