vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【TypeScript】InversifyJSでIoC(DI)体験 1

はじめに

諸般の事情から TypeScript に漬け込まれている今日この頃。

ふと TypeScript 単体で DI ってできないのかな~と思ってググった時に出てきた、 InversifyJS を試してみることにしました。

TypeScript 単体で~というのは、(今回の InversifyJS でも使われている) Decorator を使うとできそうなので、こちらも引き続き追いかけてはいきたいと思います。

なおタイトルでわざわざ IoC(Inversion of control) を出したのは、 InversifyJS の名前がそこから来てるんだろうな~と思ったためで大した理由はありません。

準備

何はともあれインストールです。

GitHub の手順に従って進めますよ。

npm install --save-dev inversify reflect-metadata

tsconfig.json

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": ["dom", "es6"],                              /* Specify library files to be included in the compilation. */

    ~省略~

    "outDir": "wwwroot/js",                        /* Redirect output structure to the directory. */

~省略~

    "strict": true,                           /* Enable all strict type-checking options. */
  
~省略~

    "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */

~省略~

    "esModuleInterop": true,                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

~省略~

    "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  }
}

コメントアウトされた部分は省略しています。また変更したところは太字にしています。

あと、ベースにしているプロジェクトは以前使ったものをそのまま再利用している関係もあり、 webpack を使っています。

exports が使えない?

以前は HTML から呼び出す関数を下記のように書いていました。

exports.doSomething = function(){
    console.log("hellllloooo");
}

が、 tsconfig.json で「"types": ["reflect-metadata"],」を有効にしようとするとエラーになってしまいました。

どういう関係があるのはよくわかっていないのですが。。。

で、下記のように変更しました。

export function doSomething(){
    console.log("hellllloooo");
}

呼び出す側は変わらず Page.doSomething() で OK です。

……いや、これできるならそもそもこれでよかったんやけど感。

ま、まぁいいや。

とりあえず DI ってみる

準備もできたところで早速使ってみましょう。

sample.ts

export interface Sample{
    call(): any;
}

DI で挿入する interface です。

特に変わったところはないです。

decoratorSample.ts

import { Sample } from "./sample";
import 'reflect-metadata';
import {injectable} from 'inversify';

@injectable()
export class DecoratorSample implements Sample{
    public call(){
        console.log("hello2");
    }
}

Sample の実装クラスです。

@injectable() なる気になるワードが。

types.ts

const TYPES = {
    Sample: Symbol.for("Sample")
}
export { TYPES }

次に登場する Container で使用します。

sampleContainer.ts

import { Container } from "inversify";
import { TYPES } from "./types";
import { Sample } from "./sample";
import { DecoratorSample } from "./decoratorSample";

const sampleContainer = new Container();
sampleContainer.bind< Sample>(TYPES.Sample).to(DecoratorSample);

export{ sampleContainer }

ここで Inject される interface と実装クラスの紐づけが行われています。

では Inject されたものを利用してみます。

mainPage.ts

import { Sample } from "./sample";
import { sampleContainer } from "./sampleContainer";
import { TYPES } from "./types";

export function doSomething(){
    let s = sampleContainer.get< Sample>(TYPES.Sample);
    s.call();
}

ASP.NET Core のコンストラクタインジェクションなどより長くなってしまっているのは気になりますが、これでインスタンスを取得できます。

ただしシングルトンではない

このままだとシングルトンではないため、上記とは別に sampleContainer.get でインスタンスを取得すると別インスタンスが渡されてしまいます。

この指定は Container で行います。

sampleContainer.ts

~省略~
const sampleContainer = new Container();
sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample)
    .inSingletonScope();
~省略~

reflect-metadata 大事

注意すべき点は Inject 対象のクラスで reflect-metadata を import することです。

これを忘れると下記のエラーが発生します。

TypeError: Reflect.hasOwnMetadata is not a function

コンパイルエラーは発生せず、見落としがちなので注意が必要です。

InversifyJS とたわむれる 1

というわけで無事 DI できました。

もう少し中身を見てみることにします。

インスタンス生成のタイミング

Inject 対象のクラスインスタンスですが、いつ生成されるのでしょうか。

decoratorSample.ts

~省略~
@injectable()
export class DecoratorSample implements Sample{
    
    public constructor(){
        console.log("Constructor death");
    }
    
    public call(){
        console.log("hello2");
    }
}

mainPage.ts

~省略~
export function doSomething(){
    
    console.log("start");
    
    let s = sampleContainer.get< Sample>(TYPES.Sample);
    s.call();
}

これを実行すると、出力結果は下記のようになります。

start
Constructor death
hello2

では Inject 対象のクラスが二つ以上ある場合はどうでしょうか。

doSomething.ts

export interface DoSomething{
    saySomething():void;
}

diDoSomething.ts

import 'reflect-metadata';
import {injectable} from 'inversify';
import { DoSomething } from './doSomething'

@injectable()
export class DiDoSomething implements DoSomething{
    public constructor(){
        console.log("DoSomething constructor death");
    }
    public saySomething():void{
        console.log("ho");
    }
}

types.ts

const TYPES = {
    Sample: Symbol.for("Sample"),
    
    DoSomething: Symbol.for("DoSomething")
    
}
export { TYPES }

sampleContainer.ts

import { Container } from "inversify";
import { TYPES } from "./types";
import { Sample } from "./sample";
import { DecoratorSample } from "./decoratorSample";
import { DoSomething } from "./decoratorSamples/doSomething";
import { DiDoSomething } from "./decoratorSamples/DiDoSomething";

const sampleContainer = new Container();
sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample)
    .inSingletonScope();
    
sampleContainer.bind< DoSomething>(TYPES.DoSomething)
    .to(DiDoSomething)
    .inSingletonScope();
    
export{ sampleContainer}

mainPage.ts

~省略~
export function doSomething(){
    
    console.log("start");
    
    let s = sampleContainer.get< Sample>(TYPES.Sample);
    s.call();
}

これを実行すると、出力結果は下記のようになります。

start
Constructor death
hello2

ということで、下記のことがわかりました。

  1. ( inSingletonScope の場合)最初に sampleContainer.get が実行されたタイミングで実体化される。
  2. sampleContainer.get が実行されていないクラスは同じ Container にバインドされていても実体化されない。

TYPES

Container への bind や get で使用されている TYPES 。

types.ts

const TYPES = {
    Sample: Symbol.for("Sample"),
    DoSomething: Symbol.for("DoSomething")
}
export { TYPES }

今回の用途である sampleContainer.get< Sample> の引数の型は string であり、その内容は Inject される interface 名となります。
(実は実装クラス名でも問題なく動作はしますが、あえてそうするメリットはなさそうです)

sampleContainer.get< Sample>("Sample") のように直接 string で指定することはできますが、引数の名前が実際のクラス名( interface 名)と違っていると実行時にエラーが発生するため、一か所にまとめるという発想は理解ができます。

が、それを const Sample: string = "Sample"; のようにせずに Symbol を使っているのはなぜでしょうか。

Symbol

そもそも Symbol とは?という話になるわけですが、上記を見たところ以下のような特徴があるようです。

  1. ECMAScript 6 から登場したプリミティブ型
  2. ユニークかつ不変である
  3. string と同じくプロパティのキーとして使用できる

特に 2.の特徴から ID として使われることから、今回も string ではなく Symbol が使われているようです。

なお、 InversifyJS の README によると、 Symbol 、 string での指定の他、 class でも指定できるようです。

ただし Symbol 推奨である、ということでした。

なお、 Symbol について注意が必要なのは、下記かなと思っています。

console.log(Symbol.for("Sample") === Symbol.for("Sample"));

console.log(Symbol("Sample") === Symbol("Sample"));

結果は下記になります。

true
false

Symbol("Sample") はユニークな値を作るのに対し、 Symbol.for("Sample") はグローバルシンボルテーブルと呼ばれる場所に値を作成し、ページをまたいだりしても Symbol.for の引数が同じであれば同じ値を返します。

そのため上記はこのようにしても true となります。

console.log(Symbol.for("Sample") == Symbol.for("Sample"));

InversifyJS については引き続き中身を追っていきたいと思います。

Symbol についても使い方が今一つよくわかってはいないため、ゆるゆると自分でも使用していきたいと思います。