inversify
inversify/InversifyJSというライブラリを使用することで、TypeScriptで制御性の反転を行うことが出来ます。これにより、TypeScriptのクラスの再利用性を高めることができ、様々なメリットを享受することが出来ます。
今回は公式サイトのチュートリアルを一通りやって見たので、その内容をまとめました。
概要
inversifyを使用するステップは以下の通りです。
- IoCコンテナで管理するクラスに
@injectable
デコレータを付与する。 - クラス、インターフェイス間の依存関係を
@inject
デコレータで表現する。 - IoCコンテナを
new
し、bind
メソッドより管理対象クラスをセットする。 - IoCコンテナに対し
get
メソッドよりオブジェクトを取得する。
inversifyの動作は非常にシンプルです。JavaのSpring Ioc Containerと概念上は同じ様な役割を持っているので、もし触ったことがあるのならすぐ理解できるでしょう。以下は、今回作成したアプリケーションの動作を簡略的に図に表したものです。
IoCコンテナで管理するクラスを作成
ステップ1と2に当たる、管理対象クラスの作成と依存関係の定義をします。ここでは以下の3つのファイルを作成しました。
ファイル名 | 内容 |
---|---|
interfaces.ts | インターフェイス定義 |
types.ts | IoCコンテナで管理されるクラスの識別子 |
entities.ts | IoCコンテナで管理されるクラス |
export interface Warrior {
fight(): string;
snake(): string;
}
export interface Weapon {
hit(): string;
}
export interface ThrowableWeapon {
throw(): string;
}
const TYPES = {
Warrior: Symbol.for("Warrior"),
Weapon: Symbol.for("Weapon"),
ThorwableWeapon: Symbol.for("ThrowableWeapon")
};
export { TYPES };
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といった設定ファイルを使用しません。(この仕様は個人的に好印象!)
基本的には以下の順序でセットアップします。
- コンテナをインスタンス化。
new Container()
- コンテナにクラスをバインド。
.bind<...>(...).to(...)
チュートリアルでは以下のようにセットアップしました。
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
メソッドに識別子を渡し、取得する対象のクラスを指定します。
今回はテストコードの形でクライアント側を実装しました。
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を使用したノウハウなどが溜まった時に再度まとめたいとおもいます。