255
241

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Angular2のDIを知る

Last updated at Posted at 2015-12-22

どうも、@laco0416です。本アドベントカレンダーでは3回目の投稿です。

Angular2のDIを知る

今回の目標は「Angular2のDIを知る」ということで、まずは基本的なところからおさらいしていきます。

Angular2のDIの使い方

まずは一番単純なDIの使い方を解説します。そもそもDIとは…という話になると複雑になるので省きますが、Angular2においては「 Providerから提供されているインスタンスを特定の変数にInject(注入)する仕組み 」のことを指します。 ProviderInject の2つの関係は重要なので頭に入れておきましょう。

Injectされる側

今回はFooServiceというクラスのインスタンスがDIで注入されるようにします。以下のコードではsay()メソッドを持つFooServiceクラスをexportしています。

fooService.ts
export default class FooService {
  
  say(): string {
    return "foo";
  }
}

Injectする側

これをComponentでDIします。DIするにはComponentのprovidersプロパティにDIしたいクラスを指定し、コンストラクタでそのクラスの引数を取ります。

import {Component} from 'angular2/core'

import FooService from "./fooService";

@Component({
  selector: 'my-app',
  providers: [FooService],
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
export class App {
  constructor(private fooService: FooService) {
    this.name = fooService.say();
  }
}

動作サンプル

コンストラクタのfooService引数にFooServiceクラスのインスタンスが注入されていることが確認できます。

どこからインスタンスが生まれているのか

簡単な使い方はわかりましたが、さて気になるのはこのfooService変数に注入されているインスタンスはどこで生まれたのかということです。

省略されているProvider

最初に述べた大事な要素の1つ Provider がインスタンスを生成する役目を持っています。しかし先程の例では一度もProviderなど作っておらず、providersプロパティにクラスを書いただけですね。なぜこれで動くのでしょうか。

実はprovidersプロパティは本来Providerの配列なのですが、クラスが入れられた時には暗黙的に内部でProviderに変換されるようになっています。先程のサンプルをProvider省略なしに書くと次のようになります。

import {Component, Provider, Inject} from 'angular2/core'

import FooService from "./fooService";

@Component({
  selector: 'my-app',
  providers: [
    new Provider(FooService, {useClass: FooService})  
  ],
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
export class App {
  constructor(fooService: FooService) {
    this.name = fooService.say();
  }
}

Plunker

変わった部分を見ていくと、providersプロパティにはProviderクラスのインスタンスが入れられているのがわかります。
Providerクラスのコンストラクタは第1引数にDIトークン、第2引数にインスタンス生成方法を指定します。ここではDIトークンとしてFooServiceを、インスタンス生成方法としてuseClassを指定しています。インスタンス生成方法に関してはまた後で詳しく触れます。

  providers: [
    new Provider(FooService, {useClass: FooService})  
  ],

Providerを省略すると、クラスをDIトークンとしたuseClassなProviderが暗黙的に生成されることを覚えておきましょう。

省略されているInject

省略されているのはProviderだけではなく、Injectも暗黙的に生成されています。Injectを省略せずに書くと次のようになります。

import {Component, Provider, Inject} from 'angular2/core'

import FooService from "./fooService";

@Component({
  selector: 'my-app',
  providers: [
    new Provider(FooService, {useClass: FooService})  
  ],
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
export class App {
  constructor(@Inject(FooService) fooService: FooService) {
    this.name = fooService.say();
  }
}

Plunker

変わったのはコンストラクタの部分です。DIを行う引数の前に@Injectアノテーションがついています。@Injectの引数にDIトークンを渡すと、マッチしたProviderからインスタンスを注入してくれます。

  constructor(@Inject(FooService) fooService: FooService) {
    this.name = fooService.say();
  }

DIトークンが引数の型アノテーションと同じである場合に、Injectが省略できることを覚えておきましょう。

インスタンスの作り方

Providerにはいくつかインスタンスの生成方法があり、先ほど登場したuseClassを含めて4つが用意されています

  • useClass
  • useValue
  • useExisting
  • useFactory

useClass: 常にnew

useClassは値としてクラスを受け取り、Injectされるたびにそのクラスのインスタンスを作り直します。イメージ的には毎回newされているような感じです。

useValue: 常に同じ値

useValueは値としてオブジェクトを受け取り、Injectされるたびにその参照を渡します。定数のように使えますが参照ごと渡ってくるのでDIされてきたオブジェクトを変更すると別の箇所に影響します。

useExisting: エイリアスを作る

useExistingは値としてDIトークンを取り、それに対するエイリアスを作ります。つまり異なるDIトークンで同じインスタンスが得られるようになります。useClassとは真逆で、一切newしません。

useFactory: なんでもあり

useFactoryの値には関数を渡します。Injectされるたびに関数が実行され、その戻り値が注入されます。

それぞれの違いを確認してみましょう。まずFooServiceを少し改造して、内部にカウンタをもたせます。

export default class FooService {
  counter = 0;
  
  say(): string {
    this.counter++;
    return `foo${this.counter}`;
  }
}

sayメソッドが呼ばれるたびにカウンタが上がり、返す文字列が変わるようになりました。

まずはuseClassでProvideした場合どうなるかです。

import {Component, Provider} from 'angular2/core'

import FooService from "./fooService";

@Component({
  selector: "hello",
  providers: [
    new Provider(FooService, {useClass: FooService})  
  ],
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
class HelloComponent {
  constructor(fooService: FooService) {
    this.name = fooService.say();
  }
}

@Component({
  selector: 'my-app',
  template: `
  <hello></hello>
  <hello></hello>
  <hello></hello>
  `,
  directives: [HelloComponent]
})
export class App {
}

Plunker

結果はこうなります

Kobito.ntSYqd.png

<hello>要素が生成されるたびにnewされるので、3つともカウンタは同じになります。

次にuseValueです。予め生成したFooServiceのインスタンスを渡してみます。

import {Component, Provider} from 'angular2/core'

import FooService from "./fooService";

@Component({
  selector: "hello",
  providers: [
    new Provider(FooService, {useValue: new FooService()})  
  ],
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
class HelloComponent {
  constructor(fooService: FooService) {
    this.name = fooService.say();
  }
}

@Component({
  selector: 'my-app',
  template: `
  <hello></hello>
  <hello></hello>
  <hello></hello>
  `,
  directives: [HelloComponent]
})
export class App {
}

Plunker

結果はこうなります

Kobito.nQ5KNc.png

同じインスタンスが渡されているのでsayメソッドが叩かれるたびにカウンタが上がっているのがわかります。ついでに上から順に処理されているのもわかります。

次はuseExistingですが、これは少し複雑です。まずFooServiceのProviderをuseClassで用意しておき、別のトークンでuseExistingを使います。

import {Component, Provider} from 'angular2/core'

import FooService from "./fooService";

class XXXService extends FooService {
  say(): string {
    return "XXX";
  }
}

@Component({
  selector: "hello",
  providers: [
    new Provider(FooService, {useClass: FooService}),
    new Provider(XXXService, {useExisting: FooService})  
  ],
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
class HelloComponent {
  constructor(service: XXXService) {
    this.name = service.say();
  }
}

@Component({
  selector: 'my-app',
  template: `
  <hello></hello>
  <hello></hello>
  <hello></hello>
  `,
  directives: [HelloComponent]
})
export class App {
}

Plunker

新たにXXXServiceクラスが登場しましたが、これはDIトークンとしてしか使われていません。コンストラクタではXXXServiceでDIしていますがprovidersでFooServiceに対してエイリアスが貼られているので出力はFooServiceのものです

Kobito.af8pAh.png

最後にuseFactoryです。次の例ではuseClassと同じ挙動になるFactoryを定義していますが、インスタンス生成後にcounterを10に設定しています。

import {Component, Provider} from 'angular2/core'

import FooService from "./fooService";

@Component({
  selector: "hello",
  providers: [
    new Provider(FooService, {useFactory: () => {
      	let instance = new FooService();
      	instance.counter = 10;
      	return instance;
      }
    })  
  ],
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
class HelloComponent {
  constructor(fooService: FooService) {
    this.name = fooService.say();
  }
}

@Component({
  selector: 'my-app',
  template: `
  <hello></hello>
  <hello></hello>
  <hello></hello>
  `,
  directives: [HelloComponent]
})
export class App {
}

Plunker

結果はこうなります。Factoryでカウンタが上がった状態でDIされているのがわかります。

Kobito.4R3NEM.png

コンポーネントツリーとDI

Angular2のDIはコンポーネントの親子関係(コンポーネントツリー)と組み合わせることでさらに強力に、柔軟に振る舞うことができます。DIを使いこなすためにもコンポーネントツリーとの組み合わせについて理解しましょう。

ここまでの例ではDIのProviderはInjectを行うコンポーネント自身のprovidersに指定していました。このprovidersは実は子のコンポーネントに継承されていきます。具体的な例を次に示します。

import {Component, Provider} from 'angular2/core'

import FooService from "./fooService";

@Component({
  selector: "hello",
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
class HelloComponent {
  constructor(fooService: FooService) {
    this.name = fooService.say();
  }
}

@Component({
  selector: 'my-app',
  providers: [FooService],
  template: `
  <hello></hello>
  <hello></hello>
  <hello></hello>
  `,
  directives: [HelloComponent]
})
export class App {
}

Plunker

Kobito.6Lige1.png

Appクラス側でFooServiceのProviderを宣言し、その子であるHelloComponent側でInjectしています。注目すべきは暗黙のuseClassにもかかわらず子のhello要素3つが同じインスタンスを参照していることです。

Providerを継承するとき、インスタンスの生成は親が行います。そしてInjectは自身のprovidersにProviderが見つからなかった時に親のprovidersを探しに行きます。JavaScriptのprototypeチェーンに似た挙動です。つまりHelloComponent側のInjectはAppがインスタンス化したFooServiceを参照するので、子はすべて同じFooServiceインスタンスを持っているわけです。

このprovidersの継承の頂点はアプリケーションの起動時に呼び出すbootstrap関数の第2引数です。Injectはコンポーネントツリーを遡り、bootstrap関数まで到達してもProviderを見つけられなかった時にようやくエラーとなります。
逆に、親でProviderを宣言していても、同じトークンで子がProviderを宣言していれば、近いほうが先にヒットするので、オーバーライドのようなことも可能です。

bootstrap関数の第2引数は必ずすべてのコンポーネントがたどり着く場所なので、アプリケーション内で同じ参照をDIしたい(いわゆるシングルトンパターン)時には、ここにuseClassで宣言するのが現在のベストプラクティスです。

bootstrap(App, [FooService]);

多段Inject

ここまでの例でDIの対象となっていたFooServiceはとても単純なクラスで、コンストラクタはデフォルトコンストラクタしか持たないのでいつでもnewでインスタンス化できました。しかしアプリケーション中では別のServiceをDIしたServiceというものが当然欲しくなります。例えば次のようにHttpモジュールを使ったBackendServiceを作るとしましょう。

backendService.ts
import {Injectable} from "angular2/core";
import {Http} from "angular2/http";

interface Person {
  name: string;
}

@Injectable()
export default class BackendService {

  constructor(private http: Http) {
    
  }
  
  getPerson(): Observable<Person> {
    return this.http.get("data/person.json")
      .map(res=>res.json() as Person);
  }
}

これを行うには@Injectableアノテーションを使う必要があります。@Injectableアノテーションがついたクラスは多段Injectへの参加権を得ることができます(以降「Injectableになる」と言うことにします)。具体的に言うと、コンストラクタの引数に対してComponentと同じようにInjectが実行されるようになります。
ただしクラスがInjectableになってもComponentもしくはbootstrapによるProvider宣言が必要なのは変わりません。BackendServiceに依存するコンポーネントは、BackendServiceとHttpどちらもProviderを宣言しなければなりません。
Httpモジュールを使う際、bootstrap関数にHTTP_PROVIDERSを渡すのは、アプリケーション全体でHttpクラスをInject可能にするためなのを理解しておきましょう。

app.ts
import {Component, Provider} from 'angular2/core'

import BackendService from "./backendService";

@Component({
  selector: "hello",
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
class HelloComponent {
  constructor(backendService: BackendService) {
    backendService.getPerson()
      .subscribe(person=>{
          this.name = person.name;
      });
  }
}

@Component({
  selector: 'my-app',
  providers: [BackendService],
  template: `
  <hello></hello>
  `,
  directives: [HelloComponent]
})
export class App {
}
main.ts
//main entry point
import "rxjs/Rx";

import {bootstrap} from 'angular2/platform/browser';
import {HTTP_PROVIDERS} from 'angular2/http';

import {App} from './app';

bootstrap(App, [HTTP_PROVIDERS])
  .catch(err => console.error(err));

Plunker

Optional

普通のDIではProviderがないとエラーを吐いてしまいますが、場合によってProviderがないかもしれない場合はOptionalにすることができます

import {Component, Provider, Optional} from 'angular2/core'

import BackendService from "./backendService";

@Component({
  selector: "hello",
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      
    </div>
  `,
  directives: []
})
class HelloComponent {
  constructor(@Optional() backendService: BackendService) {
    if(backendService){
      backendService.getPerson()
        .subscribe(person=>{
          this.name = person.name;
        });
    } else {
      this.name = "optional"
    }
  }
}

@Component({
  selector: 'my-app',
  providers: [],
  template: `
  <hello></hello>
  `,
  directives: [HelloComponent]
})
export class App {
}

Plunker

Multiple Provider

通常は同じトークンに対して複数Providerを宣言すると、最後に宣言されたものが使用されますが、multi設定されたProviderであればトークンに対してProviderを「追加」することが可能になります。プリインのPipeのProviderをまとめたPLATFORM_PIPESやDirectiveをまとめたPLATFORM_DIRECTIVESはこの機能が使われています。
ソース

import {Component, Provider, Inject} from 'angular2/core'

@Component({
  selector: "hello",
  template: `
    <div>
      <h2>Hello {{ names }}</h2>
      
    </div>
  `,
  directives: []
})
class HelloComponent {
  constructor(@Inject("Names") names: string[]) {
    this.names = names;
  }
}

@Component({
  selector: 'my-app',
  providers: [
    new Provider("Names", {useValue: "Foo", multi: true}),
    new Provider("Names", {useValue: "Bar", multi: true})
  ],
  template: `
  <hello></hello>
  `,
  directives: [HelloComponent]
})
export class App {
}

Plunker

まとめ

ProviderとInjectの対応がはっきりしており、個人的にはAngular1よりもわかりやすいDIシステムになっていると感じます。
もう少し使い込んでからDIをユニットテストなどに使ってみる実用的な知見を貯めようと思っています。みなさんも知見溜まったらぜひ共有してください。

255
241
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
255
241

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?