Angular2 における Injector の話

ちょっと機会があってみんなで Angular2 + TypeScript TUTORIAL: TOUR OF HEROES を少しずつ読んで行く勉強会をやっているのだが、コンポーネントの依存性解決に関してかなり独特な世界観があったのでちょっとまとめる。

Dependency Injection (依存性の注入)って何?

依存性とは

あるクラスに特定の変数や定数、インスタンスが入ってしまっている状態。

class Car {
    engine:Engine = new HondaEngine();
    tire:Tire = new BridgestoneTire();
}

こんな例だと「この車はHONDAのエンジンとブリヂストンのタイヤに依存している」と言える。

これだと何が問題になるか

Car クラスの動き(特定のメソッド)をテストすることを考える。

  • HONDAのエンジンとブリヂストンのタイヤを用意しなければならない。
  • もしHONDAのエンジンを用意するのに10分かかると、このテストをする度に10分待たなければならない
  • テストする度にタイヤが摩耗して減ってお金がかかる場合、タイヤ回す目的でないテストにお金がかかることになる。

などいろいろ不都合がある。
実際の開発だと「ソシャゲの課金部分を実際にお金払う仕組みに依存したらテストが有料化した」みたいなことになりかねない。

注入ってなに

メソッドの引数で、クラスや変数などを外から受け取れるようにする

class Car {
    engine:Engine;
    tire:Tire;
    constructor(_engine: Engine, _tire: Tire) {
        this.engine = _engine;
        this.tire = _tire;
    }
}

こうすると、エンジンをTOYOTAにしようが、タイヤをピレリにしようが、それをコンストラクタの引数に指定することで車が組み立てられる。この過程が注入と呼ばれる。
こうして、この車が特定の部品に依存することなく走り出すことが出来るようになる。 テストする時も、理想的なエンジンのフリをする偽エンジンと理想的なタイヤのフリをする偽タイヤを用意することで、本当に Car クラスだけの挙動を見ることができる。
そのとき、もしも不具合が起こっても「エンジンやタイヤが悪いのかも」という心配をしなくて良くなる。

Angular の Dependency Injection

さて、次は Angular2 における Dependency Injection だが、実のところ チュートリアル ではいくつかある機能のうち1個しか使っていない。
まず使ってるものに関して説明する。

@Injectable() と Angular 内部のコンテナ

まず Angular の内部にはコンテナがあり、他のコンポーネント注入 するものを入れておくことができる。
前述の Car クラスだと、エンジンやタイヤのことで、チュートリアルだと HeroService が当てはまる。
@Injectable() でデコレートしたものを、Angularが内部コンテナに取り込んでくれる。

provider プロパティと Angular 内部のコンテナからの注入

@Component({
    ...
    providers: [HeroService],
    ...
}
export class AppComponent {
    constructor(_heroService: HeroService) { }
}

こんなコードで HeroService を Angular 内部のコンテナから取り出して AppComponent に注入したと思う。
これは実は下記のシンタックスシュガー(糖衣構文。ざっくり言うと省略記法のこと)である。

@Component({
    ...
    providers: [
        new Provider(HeroService, {useClass: HeroService})
    ],
    ...
}
export class AppComponent {
    constructor(@Inject(HeroService) _heroService: HeroService) { }
}

Provider とは何ぞ?

このクラスは、指定された何か(ここではHeroService)をコンテナからコンポーネント(AppComponent) に「どうやって注入するか」を管理するクラスだ。

new Provider(A, {useClass: B}) という形で説明すると

  • A: この Providerインスタンスに付ける名前(DIトークンと呼ばれる)
  • B: 注入するクラス名

になる。useClass の部分はいろいろ変えることができて、よく使うのは下記の3個くらいだと思う。
これらのうち、どれを指定するかによって、最終的に注入されるものや方法が変わる。

  • useClass:クラス名 指定したクラスからインスタンスを作って(new)注入してくれ
  • useValue:インスタンスやオブジェクト 指定したインスタンスやオブジェクトを注入してくれ
  • useFactory:関数 指定した関数から返される値を注入してくれ

ここで、useClass 意外のものを指定してるプロパティがクラス名でないということに注目してほしい。
実は、インスタンスじゃなくて変数や定数も注入できるし、関数を指定してその結果を注入するなんてこともできる。 ここで、同じProviderからは同じインスタンスやオブジェクトが必ず返されることを覚えておくといい。 useClass を使ったプロバイダを親コンポーネントと子コンポーネントから参照しても二度 new されることはなく、 Provider 内部にキャッシュされたものを参照する。
コンポーネントProvider がなければ、親コンポーネントProvider を参照するので、子コンポーネントに同じインスタンスを注入できる、ということだ。

@Inject とは何ぞ?

@Inject(HeroService) デコレータは、上で指定したDIトークンを指定して、 どの Provider から注入してもらうかを指定するものだ。

なんで省略できたの

  • Provider : そういう決まりだから。基本的に providers にクラス名だけ書いたら、DIトークンがクラス名と同じで、かつ useClass:クラス名 を指定したことになる。
  • @Inject(HeroService) : こっちには世界観というより的確な理由がある。DIトークンが引数のクラス名と同じである場合に省略できる。

やってみるといいこと

チュートリアル に関して、以下のようなことをして動きを確認すると良い。

  • 省略記法を Provider@Inject を使った書き方に変えてみる
  • DIトークンを変えてみて、正しく動くことを確認してみる
  • HeroServiceuseValue プロパティを使った書き方に変えてみる