Angularの依存性注入とProviderについて


私のインターンシップ先の先輩が以前に
「ServiceはSingletonであることがAngular側で保証されている」
とおっしゃっているのを耳にしたことがあり、今更ながらその言葉の意味についてまとめておこうと思います。

Angular2のDIを知る

本記事は上記の記事を参考にしています。

依存性の注入(DI)

まずはAngularにおける依存性の注入についてです。

Angular2においては「 Providerから提供されているインスタンスを特定の変数にInject(注入)する仕組み 」のことを指します。 Provider と Inject の2つの関係は重要なので頭に入れておきましょう。

ここで、注入するものは「Service」、注入されるものを「Component」として進めていきます。

「Providerから提供されている」というのは、Componentデコレータのprovidersプロパティに配列として格納されているクラスのことです。
例えば、

dependency-injection.component.ts
@Component({
  selector: 'app-dependency-injection',
  templateUrl: './dependency-injection.component.html',
  styleUrls: ['./dependency-injection.component.scss'],
  providers: [DependencyInjectionService]
})
export class DependencyInjectionComponent implements OnInit {

  constructor(
    private di1: DependencyInjectionService,
  ) { }

  ngOnInit() {}
}

このようなComponentがあった時、DependencyInjectionServiceはProviderから提供されています。
そして、Component内のコンストラクタの引数に注入したいクラスの型を指定することで注入を行います。

これはAngular側の決まりで、コンストラクタ内の引数型から、注入すべきServiceをprovidersの中から決定しています。

Serviceの中身はこんな感じです。

dependency-injection.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class DependencyInjectionService {

  constructor() {    
  }

}

注入する側のクラスにはInjectableデコレータをつけます。必ずつけなければならないというわけではありませんが、つけておいた方が良いです。

Providerの役目

これでComponent内からServiceのインスタンスを扱えるようになりましたが、そもそもインスタンスの生成を行っているのはProviderです。

providersに渡す値ですが、省略せずに書くとこのようになります。

dependency-injection.component.ts
@Component({
  selector: 'app-dependency-injection',
  templateUrl: './dependency-injection.component.html',
  styleUrls: ['./dependency-injection.component.scss'],
  providers: [
    { 
      provide: DependencyInjectionService,
      useValue: DependencyInjectionService 
    }
  ]
})
export class DependencyInjectionComponent implements OnInit {

  constructor(
    private di1: DependencyInjectionService,
  ) { }

  ngOnInit() {}
}

まずprovideのバリューとして与えているのはDIトークンというものです。このトークンはServiceを特定するためのトークンです。値としてtypeが指定できますが、一般的に注入するServiceの型がそのまま指定されます。
ここでインスタンス化が行われています。

そして、コンストラクタの引数では、DIトークンを使用してServiceのインスタンスを結び付けているということです。
なので例えば、

dependency-injection.component.ts
class Foo {
}

@Component({
  selector: 'app-dependency-injection',
  templateUrl: './dependency-injection.component.html',
  styleUrls: ['./dependency-injection.component.scss'],
  providers: [
    { provide: Foo, useValue: new DependencyInjectionService() }
  ]
})
export class DependencyInjectionComponent implements OnInit {

  constructor(
    private di$: Foo,
  ) { }

  ngOnInit() {
    console.log(this.di$); // DependencyInjectionService {}
  }
}

こんなこともできてしまいます。ですが普通はこんなことはせず、素直にServiceの型をそのままDIトークンとして使います。

次に見ていくのはuseValueですが、この記事の肝となる部分なのでセクションを分けます。

サービスの生成方法について

これまでに見てきたように、Componentに注入する際にServiceのインスタンスかを行うのはproviderの役目でした。さらにproviderではどのようにインスタンスを取り扱うかも定義できます。

その定義方法は以下の4つです。

  • useClass
  • useValue
  • useExisting
  • useFactory

この内useClass、useValueの2つについてのみ詳しく見ていきます。

useValue

まず先ほどのプログラムにあったuseValueからです。これは、DIトークンに対応するオブジェクトのインスタンスをそのまま返します。つまり、この方式で作成したインスタンスは、Component内で複数のプロパティに結び付けられていても同じものとして扱うのです。

まずService側でこのような実装を行います。

dependency-injection.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class DependencyInjectionService {

  count = 0;

  constructor() {
  }

  increment() {
    this.count += 1;
  }
}

現在の時間をミリ秒で受け取って、プロパティに持たせています。
これを以下のようにしてComponent側から呼び出して見ます。

dependency-injection.component.ts
@Component({
  selector: 'app-dependency-injection',
  template: `{{ di$.count }}`,
  styleUrls: ['./dependency-injection.component.scss'],
  providers: [
    { provide: DependencyInjectionService, useValue: new DependencyInjectionService }
  ]
})
export class DependencyInjectionComponent implements OnInit {

  constructor(
    private di$: DependencyInjectionService
  ) { }

  ngOnInit() {
    this.di$.increment();

}

Componentをこのように作成して、親コンポーネントから3回呼び出してみます。
すると、

ブラウザにはこのように表示されます。これは、各Componentに紐づいているServiceインスタンスは同じであるため、Component間で関数呼び出しによる影響が及んでいることが確認できます。

useClass

ではuseClassの場合についてです。

dependency-injection.component.ts
@Component({
  selector: 'app-dependency-injection',
  template: `{{ di$.count }}`,
  styleUrls: ['./dependency-injection.component.scss'],
  providers: [
    { provide: DependencyInjectionService, useClass: DependencyInjectionService }
  ]
})
export class DependencyInjectionComponent implements OnInit {

  constructor(
    private di$: DependencyInjectionService,
  ) { }

  ngOnInit() {
    this.di$.increment();
  }

}

useClassの場合、providerでクラスが指定されるたびに新しいインスタンスの作成を行います。なので、各Componentに紐づいているインスタンスは全く別のものであり互いに影響を及ぼし合いません。

これが大きな違いです。
これを理解すると先輩がおっしゃていたことが理解できます。

ServiceはSingletonってつまりこういうこと

AngularのServiceをコマンドラインツールから作成すると、デフォルトではこんな感じになります。

typescript

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DependencyInjectionService {

  constructor() {
  }
}

provideInが肝で、これはルートのモジュール対してprovideしますよ、ということです。なので開発者側でproviderの登録をする必要は本来あまりありません。

ルートでインスタンスを作成しているので下位のComponentはインジェクターツリーの関係からインスタンスを共有します。
これはアプリケーション全体で一つのインスタンスを参照していると言い換えることができるので、それはつまりSingletonだよね、ということなのだと思います。

まとめ

こういう理解していますが、誤った理解をしている点がありましたらご指摘をお願いいたします。