vaguely

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

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

はじめに

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

続きです。

今回は Container クラスを中心に、もう少し InversifyJS の中身を見ていきたいと思います。

ContainerOptions

前回は使用しませんでしたが、 Container クラスの Constructor では引数としてオプションを指定できます。

内容は Wiki の通りなのですが、せっかくなので試してみますよ。

interfaces/interfaces.ts

~省略~
export interface ContainerOptions {
    autoBindInjectable?: boolean;
    defaultScope?: BindingScope;
    skipBaseClassChecks?: boolean;
}
~省略~

autoBindInjectable

autoBindInjectable を true にすると、下記のようにインスタンスを取得できるようになります。

sampleContainer.ts

const sampleContainer = new Container({ 
    autoBindInjectable: true
});
sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample)
    .inSingletonScope();

mainPage.ts

export function doSomething(){
  // sampleContainer.get< Sample>(TYPES.Sample) のように指定する必要がない.
  let s = sampleContainer.get(DecoratorSample);
}

一瞬良いかも、と思ってみたのですが、これだと呼び出す側も実装クラスが誰なのかを知っている必要があるため、どうなんだろう……と思ってしまいました。

もちろん私が気づいていないだけで良い使い方があるかもしれませんが。

defaultScope

defaultScope を指定すると、前回登場した inSingletonScope() などのスコープ設定をデフォルトで設定できます。

sampleContainer.ts

// Default Scope無し.
const sampleContainer1 = new Container();
sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample)
    .inSingletonScope();

// Default Scopeあり.
const sampleContainer2 = new Container({
    defaultScope:"Singleton"
});
sampleContainer2.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample);

sampleContainer1 、 sampleContainer2 ともにシングルトンで DecoratorSample のインスタンスが設定されます。

skipBaseClassChecks

一番よく分かっていない機能ですが、拡張クラスを Inject したい場合に便利らしいです。

interface について

ちょっとわき道にそれますが。

Container クラスの Constructor の引数は ContainerOption です。

が、実際には下記のように引数を渡しています。

const sampleContainer = new Container({
    defaultScope:"Singleton"
});

あれ? ContainerOption (の具象クラス) ではない?

また、先ほど見逃していましたが、 TypeScript の interface は C# とは異なり、変数(プロパティ)を持たせることもできるようです。

interfaces/interfaces.ts

~省略~
export interface ContainerOptions {
    autoBindInjectable?: boolean;
    defaultScope?: BindingScope;
    skipBaseClassChecks?: boolean;
}
~省略~

また、具象クラスを定義してインスタンスを作らなくても、 interface が持つプロパティや関数を実装していれば同じものとして扱うことができます。
(この辺りは Go などと(今も仕様が変わっていなければ)共通していますね)

例えばこういうクラスと interface があったとして。

export interface Sample{
    message: string;
    call(): any;
}
export class IntefaceSample{
    public constructor(sample: Sample){
        console.log(sample.message);
    }
}

IntefaceSample のコンストラクタに下記を渡すことができます。

let sample = {
    message: "hello",
    call: () => {}
};

let s = new IntefaceSample(sample);

先ほどの Container のコンストラクタではこの仕組みを利用していたわけですね。

あまり調子に乗って使いすぎるとわけわからん状態になりそうではありますが、こういう柔軟さは良いですね。

Container を追う

さて、 InversifyJS に戻りますよ。

まずは DI Container である(はずの) Container クラスのコンストラクタからたどっていきます。

コンストラクタの最初の処理は先ほどの ContainerOption の設定(引数で渡されていれば)を行っています。

後々 Option の指定によってどのような処理が行われるか、ということにも触れることにはなると思いますが、ここではスキップします。

で、残りの処理はこちら。

container/container.ts

~省略~
public constructor(containerOptions?: interfaces.ContainerOptions) {
~省略~
    this.id = id();
    this._bindingDictionary = new Lookup< interfaces.Binding< any>>();
    this._snapshots = [];
    this._middleware = null;
    this.parent = null;
    this._metadataReader = new MetadataReader();
}
~省略~

id

処理は utils/id.ts にあります。

やっていることは 0 からカウントアップした値を返す、というだけのシンプルなものです。

C# で Equals をオーバーライドする時に出てきた HashCode のようなものでしょうか。

_bindingDictionary

Lookup 、 Binding といういかにも重要そうなクラス、 interface が登場しました。

Lookup は container/lookup.ts 、 Binding は interfaces/interfaces.ts で定義されています。

_metadataReader

planning/metadata_reader.ts で定義されているクラスで、 metadata という名前や Reflect.getMetadata() を呼んでいたりと、 inject するクラスの取得などに関連していると思われます。
( @injectable() を付与するクラスで import が必要だった metadata_reader がコレですね)

空にしているだけであったためスキップした snapshots 、 middleware 、 parent も気になるところですが、さくさく次に進みましょう。

Container.bind

Inject する interface と具象クラスからインスタンスを取得する bind を見てみます。

container/container.ts

~省略~
    // Registers a type binding
    public bind< T>(serviceIdentifier: interfaces.ServiceIdentifier< T>): interfaces.BindingToSyntax< T> {
        const scope = this.options.defaultScope || BindingScopeEnum.Transient;
        const binding = new Binding< T>(serviceIdentifier, scope);
        this._bindingDictionary.add(serviceIdentifier, binding);
        return new BindingToSyntax< T>(binding);
    }
~省略~

まず 「const scope = this.options.defaultScope || BindingScopeEnum.Transient;」であれ?と思いましたが、これは this.options.defaultScope が undefined であった場合は BindingScopeEnum.Transient が渡されます。

ということで、何も指定しなかった場合のデフォルトのスコープは Transient になるのですね。

と思っていたら(当然ながら)ドキュメントで触れられていました。

serviceIdentifier

引数の serviceIdentifier ですが、前回使用した string 、 symbol の他 Newable つまりクラス、抽象クラスも扱うことができるようです。

interfaces/interfaces.ts

~省略~
export type ServiceIdentifier< T> = (string | symbol | Newable< T> | Abstract< T>);
~省略~

binding

bindings/binding.ts で定義されているクラスですが、ここでは先ほどの serviceIdentifier とスコープ情報を保持しています。

ここで作られた Binding< Sample>("Sample", BindingScopeEnum.Singleton) が、 container/lookup.ts の Map に格納されます。

container/lookup.ts

~省略~
private _map: Map< interfaces.ServiceIdentifier< any>, T[]>;
~省略~

この値は途中で出てきた _bindingDictionary に格納され、 Container クラスからアクセスされると。

で、この関数の中で最後に返されるのは syntax/binding_to_syntax.ts の BindingToSyntax< T> です。

BindingToSyntax< T>

BindingToSyntax クラスでは Binding< Sample>("Sample", BindingScopeEnum.Singleton) を持っています。
(コンストラクタで渡される)

んで、はじめに戻って今回作成した sampleContainer を見てみると、 to() を呼んでいます。

sampleContainer.ts

sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample);

syntax/binding_to_syntax.ts

~省略~
public to(constructor: new (...args: any[]) => T): interfaces.BindingInWhenOnSyntax< T> {
    this._binding.type = BindingTypeEnum.Instance;
    this._binding.implementationType = constructor;
    return new BindingInWhenOnSyntax< T>(this._binding);
}
~省略~

この引数で DecoratorSample (interface Sample の具象クラス)の情報が _binding に追加されます。

BindingInWhenOnSyntax

syntax/binding_in_when_on_syntax.ts で定義されているクラスです。

in when on 。。。 いえ、良いんですよ。

syntax/binding_in_when_on_syntax.ts

~省略~
    public constructor(binding: interfaces.Binding< T>) {
        this._binding = binding;
        this._bindingWhenSyntax = new BindingWhenSyntax< T>(this._binding);
        this._bindingOnSyntax = new BindingOnSyntax< T>(this._binding);
        this._bindingInSyntax = new BindingInSyntax< T>(binding);
    }
~省略~

要は in when on でそれぞれインスタンスを作っていると。

Container インスタンスを保持している sampleContainer.ts で以前のように inSingletonScope() を実行した場合、 this._bindingInSyntax が使用されるようですね。

ここまでで、 interface 、具象クラス、スコープ情報が保持されているところが何となく見られてような気がします。

スコープの種類にかかわらず、実際にインスタンスが作られるのは最初に Container にインスタンスを取りに行ったタイミング。

ということで、 get を追うといよいよインスタンスが生成されたり保持されたりするところが見られる、ということ。。。だと思います。多分。