はじめに
DIコンテナ自体は特に目新しい技術ではありません。JavaScript界隈ではAngularJS 1.xやRequireJS(AMD)等はそれ自体がDIの仕組みを内包したライブラリです。
しかし、これらのDIは若干無理やりな実装方法を取っていた感があります12。これはJavaScriptでメタデータやAOPを適切に扱う機能が不足していたことが背景にあると考えているのですが、ここ1, 2年で言語側の状況も変化してきています。
具体的にはTypeScript 1.5からDecoratorsがサポートされたり、ES 2015にてリフレクションの仕様が追加されたりと、よりスマートなDIコンテナを実装するための基盤が整いつつあります。
そこで今日はInversifyJSという軽量JavaScript DIコンテナについて触れるとともに、最新のDI事情を見ていきたいと思います。
InversifyJSについては、少し前にgithubのtrendに上がっていたので気にはしていたんです。元々JavaをやってたときからDIコンテナ系の技術が好きというのもあります。
ただ、キャッチアップした当初のInversifyJSはAngularJS 1.xのような name-based なDI機構が提供されるのみであったため、あまり使いたいとは思っていませんでした。
そんなことをtwitterで愚痴ってたら、2.0.0-alpha.8でclass-based DI機構があれよあれよという間にmergeされたため、もう一度触ってみようと思った次第です。
InversifyJS自体はJavaScript, TypeScript両方をターゲットにしているDIコンテナですが、今日はclass-basedなDIの話を書く都合上、静的な型付言語であるTypeScriptを選択します。
「TypeScript におけるclass-based DI」と言えばAngular2が有名ですが、DIのみ欲しいときにAngular 2は大仰に過ぎます。一方、InversifyJSは軽量というだけありはソースコードも充分読み易い量ですのでDIを学ぶには打ってつけだと思います。
InversifyJSを触ってみる
まずはinstallです。reflect-metadataを忘れずに。
npm init -y
npm install inversify@2.0.0-alpha.8 reflect-metadata --save
続いてTypeScriptの設定をしましょう。
tsc --init
生成されたtsconfig.jsonをエディタで開き、experimentalDecorators, emitDecoratorMetadata, moduleResolutionを下記のように設定しておきます:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"noImplicitAny": false,
"sourceMap": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node"
},
"exclude": [
"node_modules"
]
}
これでInversiffJSを利用する環境が整いました。ソースコードを書いていきます。
///<reference path="node_modules/inversify/type_definitions/inversify/inversify.d.ts" />
import 'reflect-metadata';
import {injectable, Kernel} from 'inversify';
// 注入したい振る舞いの定義
@injectable()
abstract class AwesomeService {
abstract greeting(): string;
}
// 振る舞いに対応した実装
@injectable()
class ConcreteAwesomeService extends AwesomeService {
greeting() {
return "It's a concrete awesome service!";
}
}
// 注入される側のclass
@injectable()
class Main {
constructor(private service: AwesomeService) {}
run() {
console.log(this.service.greeting());
}
}
// DIコンテナの準備
const kernel = new Kernel();
kernel.bind<AwesomeService>(AwesomeService).to(ConcreteAwesomeService);
kernel.bind<Main>(Main).to(Main);
// コンテナからインスタンスを取得
const main: Main = kernel.get<Main>(Main);
main.run();
tsc -p . && node app.js
コンソールに It's a concrete awesome service!
が出力された筈です。
ちょっとだけ解説
さすがに「DIが動きました!やったね!」でこのエントリを締めくくってしまうとあまりにもアホっぽいので、もう少しInversifyJSのDI機構について書いていこうと思います。
DI(Dependency Injection)って何なの?とかInversion of ControllだとかSOLIDとDIPだのについては、インターネッツに山のように情報が溢れていますのでここでは語りません。
先ほどのコード例では、AwesomeService
, ConcreteAwesomeService
, Main
という3つのclassが登場しており、次の関係を満たしています:
-
Main
という上位レイヤーはAwesomeService
を利用しているが、AwesomeService
の実体コードは気にしない - 実際に動作する
ConcreteAwesomeService
は 抽象型であるAwesomeService
に依存している
特筆すべきは、Main -> AwesomeServiceの依存関係が、型情報のみを用いて解決されているという点です。この部分の仕組みを追いかけてみます。
InversifyJSに限った話ではないですが、DIコンテナは「一般的に注入されるモノ」を管理するための辞書を内部的に保持しています(InversifyJSの場合、Lookup class に辞書の実体が存在しています)。DIコンテナの利用者は、この辞書に対して値の登録を行う必要があります。
先ほどのInversifyJSのサンプルコードでは下記の部分が辞書への登録に該当します。
kernel.bind<AwesomeService>(AwesomeService).to(ConcreteAwesomeService);
kernel.bind<Main>(Main).to(Main);
- キー
AwesomeService
に対して、値ConcreteAwesomeService
を登録 - キー
Main
に対して値Main
を登録
抽象クラスのconstructor関数をキーとして、具象クラスのconstructor関数を値としたMapですね。
DIコンテナに登録された Main classのインスタンスを利用する場合は、kernel.get(Main)
とすることで辞書に登録した情報を元にDIコンテナがconstructorを解決してくれます。
@injectable()
class Main {
constructor(private service: AwesomeService) {}
/* 中略 */
}
Main classのconstructorには AwesomeService
型の推移依存が記載されており、AwesomeService
型は 先述のkernel.bind(AwesomeService)
にてkernelに登録済みです。
やはり kernel.get(AwesomeService)
を実行することで ConcreteAwesomeService
のインスタンスが取得可能です。このように、constructor引数の型をキーとして再帰的にkernelからのインスタンス取得を繰り返していくことで、最終的にMain classのインスタンスが取得できるわけです。
ここで「service: AwesomeService
の型情報はTypeScriptのソース上は存在していても、.jsにコンパイルされたら消えてしまうんじゃない?」という疑問が湧いてきます。
答えはNoです。Main classはTypeScriptによってコンパイル(desugar)したJavaScriptは次のes6コードと等価です(可読性のために実際のコンパイル結果とは異なるコードを記載):
class Main {
constructor(private service) {}
run() {
console.log(this.service.greeting());
}
}
Reflect.metadata('design:paramtypes', [AwesomeService])(Main);
injectable()(Main);
末尾2行がポイントです:
Reflect.metadata('design:paramtypes', [AwesomeService])(Main);
injectable()(Main);
-
Reflect.metadata
,injectable
はclassのconstructor関数を引数にとる関数を返します。これは Class Decorator3の形式ですので、TypeScriptのコード上は@inject()
のように呼びだす事ができたということです。tsconfig.jsonにexperimentalDecorators
を設定したのはこのためです。 -
Reflect.metadata
は reflect-metadata モジュールの機能です。'design:paramtypes' という特殊なキーが、「関数引数の型情報」を意味しています。 -
injectable
は内部でReflect.getMetadata
を呼び出し、'design:paramtypes' から型情報を回収し、コンテナへ連携しています4。 -
Reflect.metadata
の行はClass Decoratorが利用された場合にTypeScriptが付与します(tsconfig.jsonにemitDecoratorMetadata
が設定されている場合のみ)。
ReflectとDecoratorsが大活躍です5。冒頭の環境構築時の説明では何も触れませんでしたが、npm install reflect-metadata
としたり、tsconfig.jsonに設定を追記していたのは、この2行の為だったということです。
試しにtsconfig.jsonのemitDecoratorMetadata
をfalseに設定してみて再度実行してみて下さい。service.greeting()
の呼び出しでコケるはずです。
なお、emitDecoratorMetadata
が有効であっても、constructor引数の型がinterfaceで定義されている場合はInversifyJSのDIは動作しません。
というより、kernel.bind
にてinterfaceをbindすることすら出来ません。どうしても引数の型にinterfaceを使った状態でDIを行いたければ、Symbol を使うなりの解決方法が用意されています。interafaceに固有のSymbolを用意する必要があったりと若干手間です。
おわりに
今回のエントリでは、InversifyJSによるclass baseなDIの基本を述べました。またclass base DIを実現するために必要な「どのようにDIコンテナへ型情報を伝えるか」の部分を解説しました。TypeScriptでAngular2を使う場合も reflect-metadata とtsconfigのexperimentalDecorators
, emitDecoratorMetadata
は必須になるのですが、DIコンテナの実装は違えど、これらの設定が必要になる理由は全く一緒です。
InversifyJSの詳細な使い方については特に詳しくは触れませんでしたが、APIから察するにJavaの軽量DIコンテナであるgoogle/guiceの影響が色濃く見て取れます。kernel.bind(...).to(...).inSingletonScope()
のようにメソッドチェーンでinterface絞っていく所とかそっくりです。
DIの典型的なユースケースは「特定のserviceについてテスト時はモックに差し替える」等がありますが、kernelのbind方法を押えておけば、あとはmochaやjasmineのようなテスティングフレームワークと組み合わせるだけで簡単に実現できると思います。
-
AngularJS 1.xにおけるDI機構の裏側は AngularJSのDIの仕組み、minify対策は覚えておこう! が詳しいです ↩
-
Dependency injection in JavaScript にRequireJS, AngularJSのDI実装方法の概要が紹介されています ↩
-
class-based DIとかname-based DIと呼び分けていますが、このキーワードでググっても殆ど何もヒットしません。今回の記事では型情報を用いてDIを行う手法をclass-based DI, 名前情報を用いてDIを行う手法をname-based DIと呼ぶことにします ↩
-
TypeScriptのClass DecoratorについてはTypeScriptのDecoratorメモにも記事を投稿しています。 ↩
-
https://github.com/inversify/InversifyJS/blob/master/src%2Fannotation%2Finjectable.ts ↩