概要
とある経緯でTypescriptでDIフレームワークについて調べることがあったのでそのまとめ。
比較対象として、InversifyJSとTSyringeの2つを触ってみたのでその備忘録。
1.InversifyJS
READMEに書かれている通り強力なDIフレームワーク。TypescriptのDIフレームワークで調べると結構これが出てくるし、実際にgithub starの数も他と比べるとかなり多い。
使い方
DIされる側は以下のようにインターフェースの実装クラスに@Injectable()
を付ける。
// インターフェース
export interface Employee {
work(): void;
}
import { Employee } from './Employee';
import { injectable } from 'inversify';
import 'reflect-metadata';
// 実装クラス
@injectable()
export class BoardGameLover implements Employee {
work(): void {
console.log('ずっとボードゲームを遊んでいる.');
}
}
利用側では@Inject
をつける。
import { Employee } from './Employee';
import { Shop } from './Shop';
import { injectable, inject } from 'inversify';
import { TYPES } from './types';
@injectable()
export class BoardGameShop implements Shop {
private employee: Employee;
// コンストラクタインジェクション
constructor(@inject(TYPES.Employee) employee: Employee) {
this.employee = employee;
}
watchEmployee() {
this.employee.work();
}
}
@Inject
に渡しているTYPES.Employee
はSymbolを渡していて、別途types.tsというファイルに宣言している。これによってDIされるオブジェクトをリテラルで書かずにSymbolでやりとりしている。
const TYPES = {
Employee: Symbol.for('Employee'),
Shop: Symbol.for('Shop')
}
export { TYPES }
DIコンテナに登録するオブジェクトは以下のようなConfigファイルにまとめて記載をし、ここでインターフェース、シンンボル、DIされる実クラスの紐付けを行っている。
import { BoardGameLover } from './BoardGameLover';
import { Employee } from './Employee';
import { BoardGameShop } from './BoardGameShop';
import { Shop } from './Shop';
import { Container } from "inversify";
import { TYPES } from "./types";
const myContainer = new Container();
myContainer.bind<Shop>(TYPES.Shop).to(BoardGameShop);
myContainer.bind<Employee>(TYPES.Employee).to(BoardGameLover);
export { myContainer };
コンテナから取り出すときは以下のようなかたち
const shop = myContainer.get<Shop>(TYPES.Shop);
感触
READMEに書かれている利用法はシンプルだが、この他にもプロパティインジェクション(フィールドインジェクション)があったり、Container
クラスのAPIが充実していたりとかなり多機能。バックエンドをクリーンアーキテクチャでがっつり作るとかには向いてそう。
しばらく前からメンテされてなさそうな雰囲気が残念(2020/3現在)。ロゴがかっこいい。
2. TSyringe
マイクロソフトが出しているDIフレームワーク。InversifyJSに比べるとこちらはかなり軽量で、コンストラクタインジェクションにフォーカスを絞っている。機能もドキュメントもとにかく小さい。
使い方
基本的なアノテーションはほぼ同じで、@injectable
, @Inject
を使って定義をする。
DIするオブジェクト
// インターフェース
export interface Employee {
work(): void;
}
import { Employee } from "./Employee";
import {injectable} from "tsyringe";
// 実装クラス
@injectable()
export class BoardGameLover implements Employee {
work(): void {
console.log("ずっとボードゲームを遊んでいる.");
}
}
利用側
import { Shop } from './Shop';
import { Employee } from "./Employee";
import { injectable, inject } from "tsyringe";
@injectable()
export class BoardGameShop implements Shop{
employee: Employee;
// コンストラクタインジェクション
constructor(@inject("Employee") employee: Employee) {
this.employee = employee;
}
watchEmployee() {
this.employee.work();
}
}
コンテナの宣言は以下のような感じ。Inversifyと違いインターフェースを被っているコンポーネントのコンテナ登録はリテラルで行われるので保守しやすさを意識するなら別途types.tsみたいなファイルを宣言するのが良さそう。
import "reflect-metadata";
import {container} from "tsyringe";
import { Employee } from "./Employee";
import { BoardGameLover } from "./BoardGameLover";
import { Shop } from "./Shop";
import { BoardGameShop } from "./BoardGameShop";
// コンテナ定義
container.register("Employee", {useClass: BoardGameLover})
container.register("Shop", {useClass: BoardGameShop})
const shop: BoardGameShop = container.resolve("Shop")
console.log("店員の様子をチェック.")
shop.watchEmployee();
感触
基本的なDIの仕方はInversifyJSと同じで、こっちのほうがより提供機能が絞られている。プロパティインジェクションとかは明示的にやらないと宣言されていたり、本当にシンプルなDIフレームワークを目指しているようには見える。
ドキュメントはほとんどなくて、実質READMEだけという状態。これから伸びていけば充実してくかもしれない。とりあえずコンストラクタインジェクションが使えるようにしたい、であればこちらのほうがいいかもしれない。