inversifyJSを用いてTypeScriptでDIをする

この記事では、InversifyJS を用いて TypeScript で DI をするサンプルを紹介し、また、(DIに慣れていない方向けに)DI をすると何が嬉しいのか、というのを説明します。

サンプル

「ユーザーの登録」「登録したユーザーの情報を取得」の 2 つのシンプルな機能を持ったプログラムを書きます。 また、ユーザーが登録されたときにログされるようにします。

準備

必要な依存をインストールします。

$ npm i inversify reflect-metadata typescript ts-node

tsconfig.jsonを以下の要領で作成します。 types": ["reflect-metadata"], "experimentalDecorators": true, "emitDecoratorMetaData": trueというのが InversifyJS のために必要です。

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es6", "dom"],
    "types": ["reflect-metadata"],
    "module": "commonjs",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

コード

src/User.ts

User クラスを定義します。 プロパティ2つとtoString()関数のみのシンプルなクラスです。

class User {
  constructor(public id: number, public name: string) {}

  toString(): string {
    return `User${JSON.stringify({ id: this.id, name: this.name })}`;
  }
}

src/UserRepository.ts

User クラスに対応付けられるデータの永続化に用いるためのインターフェイスを定義します。 「ユーザーの作成」と「ユーザーの取得」の 2 つの操作を定義しています。

import User from "./User";

export default interface UserRepository {
  find(id: number): User;
  create(name: string): User;
}

src/UserRepositoryImpl.ts

UserRepository の実装を用意します。 通常のアプリケーションであればここでデータベースなどを利用した実装をするかと思いますが、今回はシンプルにインメモリのオブジェクトを用いています。

後に DI で他オブジェクトに注入できるよう、inversify の@injectable()デコレータをつけています。

import { injectable } from "inversify";
import User from "./User";
import UserRepository from "./UserRepository";

@injectable()
export default class UserRepositoryImpl implements UserRepository {
  private store: { [key: string]: User | undefined } = {};
  private nextUserId: number = 0;

  constructor() {}

  find(id: number): User {
    const user = this.store[id];
    if (!user) {
      throw new Error("user not found");
    }
    return user;
  }

  create(name: string): User {
    const id = this.nextUserId++;
    const user = new User(id, name);
    this.store[id] = user;
    return user;
  }
}

src/Logger.ts

こちらはロガー用のシンプルなインターフェイスです。

export default interface Logger {
  log(message: string): void;
}

src/LoggerImpl.ts

ロガーの実装です。実際は winston などを使ったりするとは思いますが、ここでは console に流すだけにしています。 こちらも@injectable()デコレータをつけています。

import { injectable } from "inversify";
import Logger from "./Logger";

@injectable()
export default class LoggerImpl implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

src/App.ts

メインのクラスを定義します。

createNewUser(name: string)により、(UserRepository インターフェイスを実装したインスタンスによって)ユーザーが作成され、また新しいユーザーが作成されたことが(Logger インターフェイスを実装したインスタンスを用いて)ログされるようにしています。

findUser(id: number)により、特定の id に対応するユーザーのデータを取得できるようにしています。

@inject()デコレータによって、userRepositoryUserRepositoryのインスタンスが、loggerLoggerのインスタンスが注入されるようになっています。

import { injectable, inject } from "inversify";

import UserRepository from "./UserRepository";
import Logger from "./Logger";

@injectable()
export default class App {
  private readonly userRepository: UserRepository;
  private readonly logger: Logger;

  constructor(
    @inject("UserRepository") userRepository: UserRepository,
    @inject("Logger") logger: Logger
  ) {
    this.userRepository = userRepository;
    this.logger = logger;
  }

  createNewUser(name: string) {
    const user = this.userRepository.create(name);
    this.logger.log(`new user: ${user}`);
    return user;
  }

  findUser(id: number) {
    const user = this.userRepository.find(id);
    return user;
  }
}

src/inversify.config.ts

DI コンテナを作成し、使用するオブジェクトをバインドします。

import { Container } from "inversify";

import App from "./App";
import Logger from "./Logger";
import LoggerImpl from "./LoggerImpl";
import UserRepository from "./UserRepository";
import UserRepositoryImpl from "./UserRepositoryImpl";

const container = new Container();
container.bind<Logger>("Logger").to(LoggerImpl);
container
  .bind<UserRepository>("UserRepository")
  .to(UserRepositoryImpl)
  .inSingletonScope();
container.bind<App>("App").to(App);

export default container;

src/index.ts

実際に実行させてみるコードです。

import "reflect-metadata";

import container from "./inversify.config";
import App from "./App";

container.get<App>("App").createNewUser("Taro");
container.get<App>("App").createNewUser("Jiro");
console.log(container.get<App>("App").findUser(1)));

実行

実際に実行させてみると、以下のようになり、うまくいっていることがわかります。

$ ./node_modules/.bin/ts-node srd/index.ts
new user: User{"id":0,"name":"Taro"}
new user: User{"id":1,"name":"Jiro"}
User { id: 1, name: 'Jiro' }

inversifyJS、直感的でわかりやすいですね。

補足

DIコンテナへのオブジェクトの登録において、簡単のために文字列を用いましたが、Symbolを利用することが推奨されています。

何が嬉しいのか

オブジェクト指向的なプログラミングをする際、インターフェイス等を用いて「実装は抽象に依存させる」こと、そして「できるだけ抽象への依存を増やす」ことが大切です。 そうすることでオブジェクト同士が疎結合になり、テストもしやすく、各オブジェクトの実装の変更が簡単な設計になります。

例えば、今回のAppクラスはLoggerとUserRepositoryという抽象(インターフェイス)に依存をしていました。 そして、LoggerImplという実装はLoggerという抽象に依存し、UserRepositoryImplという実装はUserRepositoryという抽象に依存していました。

このことにより、AppクラスはLoggerの実装やUserRepositoryの実装と切り離されていました(疎結合)。

実際、UserRepositoryに関して、(今回や、テスト時のために)単純なオブジェクトを用いた実装を用意しても、MySQLを用いた実装を用意しても、Appクラスのコードは全く変わらないことがわかると思います。

このように、インターフェイスを用いて依存の方向を制御するとソフトウェアの構成がいい感じにはなるのですが、オブジェクトの作成・その受け渡しが結構面倒になります。

今回のケースだったら、

const logger = new LoggerImpl();
const userRepository = new UserRepository();
const app = new App(userRepository, logger);

程度で済んだりもしますが、オブジェクトの数が増え、依存性も複雑になってくるとかなりしんどくなってくるはずです。

それの面倒を見てくれるのが、inversifyJSのようなDIフレームワーク、DIコンテナというわけです。

参考

inversify.io

github.com