JavaScript&Node.js環境でDependency Injectionができるinversifyを試してみた


inversify

inversify/InversifyJSというライブラリを使用することで、TypeScriptで制御性の反転を行うことが出来ます。これにより、TypeScriptのクラスの再利用性を高めることができ、様々なメリットを享受することが出来ます。

今回は公式サイトのチュートリアルを一通りやって見たので、その内容をまとめました。

概要

inversifyを使用するステップは以下の通りです。

  1. IoCコンテナで管理するクラスに@injectableデコレータを付与する。
  2. クラス、インターフェイス間の依存関係を@injectデコレータで表現する。
  3. IoCコンテナをnewし、bindメソッドより管理対象クラスをセットする。
  4. IoCコンテナに対しgetメソッドよりオブジェクトを取得する。

inversifyの動作は非常にシンプルです。JavaのSpring Ioc Containerと概念上は同じ様な役割を持っているので、もし触ったことがあるのならすぐ理解できるでしょう。以下は、今回作成したアプリケーションの動作を簡略的に図に表したものです。

IoCコンテナで管理するクラスを作成

ステップ1と2に当たる、管理対象クラスの作成と依存関係の定義をします。ここでは以下の3つのファイルを作成しました。

ファイル名 内容
interfaces.ts インターフェイス定義
types.ts IoCコンテナで管理されるクラスの識別子
entities.ts IoCコンテナで管理されるクラス
src/interfaces.ts
export interface Warrior {
  fight(): string;
  snake(): string;
}

export interface Weapon {
  hit(): string;
}

export interface ThrowableWeapon {
  throw(): string;
}
src/types.ts
const TYPES = {
  Warrior: Symbol.for("Warrior"),
  Weapon: Symbol.for("Weapon"),
  ThorwableWeapon: Symbol.for("ThrowableWeapon")
};

export { TYPES };
src/entities.ts
import { injectable, inject } from "inversify";
import "reflect-metadata";

import { ThrowableWeapon, Warrior, Weapon } from "./interfaces";
import { TYPES } from "./types";

@injectable()
class Katana implements Weapon {
  public hit(): string {
    return "cut!";
  }
}

@injectable()
class Shuriken implements ThrowableWeapon {
  public throw(): string {
    return "hit!";
  }
}

@injectable()
class Ninja implements Warrior {

  private weapon: Weapon;

  private throwableWeapon: ThrowableWeapon;

  public constructor(
    @inject(TYPES.Weapon) weapon: Weapon,
    @inject(TYPES.ThorwableWeapon) throwableWeapon: ThrowableWeapon
  ) {
    this.weapon = weapon;
    this.throwableWeapon = throwableWeapon;
  }

  public fight(): string {
    return this.weapon.hit();
  }

  public snake(): string {
    return this.throwableWeapon.throw();
  }
}

export { Katana, Shuriken, Ninja };

注目して欲しいのはentities.tsの内容です。このファイルで定義されているクラスはIoCコンテナで管理されるクラスなので@injectableデコレータを付与します。また、Ninjaクラスのコンストラクタ引数では@injectデコレータを使用し依存性を注入(インジェクション)しています。この時、types.tsで定義されている識別子を使用し、IoCコンテナに対して取得したいクラスが何かを伝えます。(識別子によって解決されるクラスの設定はこの後説明します)

識別子はSymbol以外に文字列やクラスを使用することもできます。しかし、公式ではSymbolを使用することが推奨されています。

ここでクラス間に依存関係がないことが分かります。クラスが依存しているのはインターフェイスのみで、その実装には依存していません。これにより、それぞれのクラスの独立性が高まります。

TIPS:プロパティを使用したインジェクション

コンストラクタではなく、プロパティに直接依存性をインジェクションするには以下のように@injectをプロパティに書きます。

@injectable()
class Ninja implements Warrior {

    @inject(TYPES.Weapon) private katana: Weapon;

    @inject(TYPES.ThrowableWeapon) private shuriken: ThrowableWeapon;

    public fight() { return this.katana.hit(); }

    public sneak() { return this.shuriken.throw(); }
}

IoCコンテナの作成と設定

ステップ3に当たるIoCコンテナの作成と設定を行います。IoCコンテナは実行可能なコードで表現しXMLといった設定ファイルを使用しません。(この仕様は個人的に好印象!)

基本的には以下の順序でセットアップします。

  1. コンテナをインスタンス化。new Container()
  2. コンテナにクラスをバインド。.bind<...>(...).to(...)

チュートリアルでは以下のようにセットアップしました。

src/inversify.config.ts
import { Container, ContainerModule, interfaces } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";
import { Ninja, Katana, Shuriken } from "./entities";

const container = new Container();
container.bind<Warrior>(TYPES.Warrior).to(Ninja);
container.bind<Weapon>(TYPES.Weapon).to(Katana);
container.bind<ThrowableWeapon>(TYPES.ThorwableWeapon).to(Shuriken);

export { container };

作成されたコンテナはクライアントから直接使用されるため、エクスポートする必要があります。

ここで注目すべきはbindメソッドの使い方です。bindメソッドを使いコンテナにクラスを登録します。書き方は以下の通りです。

container.bind<"取得する時の型">("識別子").to("登録対象クラス")

IoCコンテナからオブジェクトを取得

クライアントからIoCコンテナよりオブジェクトを取得します。getメソッドに識別子を渡し、取得する対象のクラスを指定します。

今回はテストコードの形でクライアント側を実装しました。

inversely.spec.ts
import { container } from "../../src/basic/inversify.config";
import { TYPES } from "../../src/basic/types";
import { Warrior } from "../../src/basic/interfaces";
import { Ninja } from "../../src/basic/entities";

import { expect } from "chai";
import "mocha";

describe("Container API", () => {
  it("get instance from container", () => {
    // Execute
    const warrior = container.get<Warrior>(TYPES.Warrior);

    // Verify
    expect(warrior.fight()).to.be.eql("cut!");
    expect(warrior.snake()).to.be.eql("hit!");
  });
});

上記のコードではgetメソッドに識別子であるTYPES.Warriorを指定し、Warriorインターフェイスの実装であるNinjaクラスの実態を取得しています。しかし、クライアント側であるテストコードはその実態がNinjaクラスである事を知る必要はありません。これにより、クライアント側はインターフェイスのみに依存することができ、Warriorインターフェイスの実装を自由に切り替えることが出来るようになりました!

感想

TypeScriptを使用した開発では、クラスによるモジュール化を自然と押し進めることになると思います。そこでは当然クラス間の依存関係をどうするのかという課題が残るわけですが、解決の一つの選択肢としてinversifyは非常に強力なものになると思います。今後inversifyを使用したノウハウなどが溜まった時に再度まとめたいとおもいます。