🍣

Angular の DI の仕組みについて理解する

2022/03/04に公開

Angular の DI の仕組み

Angular を用いたアプリケーション開発では、
Angular の内部で整備された DI コンテナを使って、
DI を活用したコードを記述することが可能です。

よく見られる、以下のようなコードでは、
コンポーネントの内部にコンストラクタ経由で MessageService を注入しています。

@Component({ ... })
export class AppComponent {
  name = 'Angular ' + VERSION.major;

  constructor(private message: MessageService) {}

  hello() {
    this.message.hello();
  }
}

上記のようなコードを書いたとき、 Angular は以下のような手順でコンポーネントを生成します。

  1. Angular がコンポーネントの生成を始める
  2. コンポーネントの生成時に、コンストラクタを確認し、記載されている必要なオブジェクトの一覧をリストアップする
  3. 必要なオブジェクトを DI コンテナから取得する。
  4. 取得したオブジェクトを利用してコンポーネントを作成する。

DI コンテナは、Angular 上での様々なクラス生成時に、必要なオブジェクトをとってくる箱として機能しています。
Angular の DI では、この DI コンテナに必要なオブジェクトを詰め込み、オンデマンドで要素を利用する、といった方法で DI が実現されています。

DI コンテナへの登録

Angular の DI コンテナに、要素を登録する方法はいくつかあります。

1つは module の定義に追記する方法です。
module 定義の providers のセクションに サービスを登録することで、DI 上必要なタイミングでオブジェクトをコンテナから取得することができるようになります。

@NgModule({
  imports: [ ... ],
  declarations: [ ... ],
  bootstrap: [ ... ],
  providers: [ MessageService ],
})
export class AppModule {}

また、サービス宣言時に、Injectable デコレータを利用して、 module 上の登録なしにサービスをコンテナ経由で利用することもできます。

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

@Injectable({
    providedIn: 'root',
})
export class MessageService {
    ...
}

Injectableの記法は Angular6 から追加された特別な(後付けの)記法であり、基本的な DI のフローは モジュール経由で コンテナに登録して、コンポーネントから利用する、というフローである、ということを理解しておきましょう。

DI のメリット

DI コンテナを利用することで、依存要素の差し替えが簡単に実現できるようになります。

以下のような形で、アプリケーションの様々な場面で利用されている、MessageServiceクラスの存在を考えてみましょう。

@Component({ ... })
export class AppComponent {
  name = 'Angular ' + VERSION.major;

  constructor(private message: MessageService) {}

  hello() {
    this.message.hello();
  }
}

MessageService.hello の内容を少し書き換えた別の内容に変更したい場合、DI を活用している設計では、別の MessageService を作成して 外側から差し替える、といった手法を用いることがあります。

例えば、MessageService を修正した、MessageJpService を作成した場合、module で以下のような記述を行って、オブジェクトの差し替えを行います。

@NgModule({
  imports: [ ... ],
  declarations: [ ... ],
  bootstrap: [ ... ],
  providers: [{ provide: MessageService, useClass: MessageJpService }],
})
export class AppModule {}

拡張された providers 構文では、 provide で DI 上から呼び出すときの名前を、useClass で実際に利用するオブジェクトを指定しています。
上記の例では、MessageService を DI 上で参照したときに、MessageJpService を利用するという宣言になっています。

上記のモジュール定義を行うと、アプリケーションの中で、DI から MessageService を呼び出している様々な箇所で、コンポーネント類の記述を書き換えることなく、MessageJpService が利用されるようになります。

この機能を利用することで以下のような様々な場面で、サービスの差し替えを利用してスムーズな開発を進めることができます。

  • アプリケーション全体で利用されている依存オブジェクトを、リリース後の運用で一時的に差し替えるような場面
  • アプリケーション全体で利用されている依存オブジェクトを、テスト実行時にモックに差し替えるような場面

Tips

糖衣構文の構造を理解する

モジュールの定義で紹介した、以下の記述は、糖衣構文です。

@NgModule({
  ...
  providers: [ MessageService ],
})
export class AppModule {}

構造的には以下のように記述するのと同じ効果があります。

@NgModule({
  ...
  providers: [{ provide: MessageService, useClass: MessageService }],
})
export class AppModule {}

クラスではない値の注入

クラスではない値を DI で利用するには、InjectionToken オブジェクトを利用します。

https://angular.jp/guide/dependency-injection-providers#injectiontokenオブジェクトの使用

参考

https://angular.jp/guide/architecture-services
https://angular.jp/guide/dependency-injection-providers

Discussion