Chapter 07

2-1.依存性の注入

lacolaco
lacolaco
2023.04.08に更新

依存性の注入はオブジェクト指向プログラミングにおけるもっとも重要な設計パターンのひとつです。Angular では依存性の注入を使わずにアプリケーションを作ることはないと言ってもいいでしょう。コンポーネントやディレクティブ、サービスといった Angular の機能は、依存性の注入を使って機能するように設計されています。依存性の注入について理解せずに Angular アプリケーションをうまく開発することはできないでしょう。

この章では、今後の学習や実践をスムーズにするために、Angular が提供する依存性の注入の機能について基本的な解説を行います。

依存性の注入を使う理由

オブジェクト指向プログラミングは長い歴史を持ち、いくつもの優れた設計パターンを生み出してきました。依存性の注入はその中でも特に重要なもののひとつです。Angular はフレームワークの基本的な機能として依存性の注入のための API を提供していますが、それだけではなく、Angular が提供するさまざまな機能は、依存性の注入を介して機能するように作られています。したがって、Angular アプリケーションの開発において依存性の注入は避けて通れません。

だからといって、Angular アプリケーション全体を依存性の注入を最大限に駆使して実装しなければならないわけではありません。それぞれの開発者・チームの経験や好み、アプリケーションの特性に応じて、依存性の注入をどのように活用するかは異なります。Angular が依存性の注入を機能として提供しているからといって、それを「使いこなす」ことにこだわる必要はありません。

ただし、実際の Angular アプリケーション開発では依存性の注入が広く活用されています。それだけ汎用的で強力な設計パターンであり、同じ設計パターンを共有しているということがコミュニティで知識や経験を共有しやすくなることにも繋がっています。使い所を見極めた上で依存性の注入を使うことで、変更に強く、テストしやすいしなやかなアプリケーションを作ることができるでしょう。

この章では依存性の注入そのものの基本的な解説はしません。もし依存性の注入そのものについて深く学びたいのであれば、この設計パターンの名付け親である Martin Fowler 氏が書いた Inversion of Control Containers and the Dependency Injection pattern という記事を読みましょう。日本語訳も書かれているので、英語が苦手な方はそちらを読んでみてください。

Angular における依存性の注入

Angular における依存性の注入を構成する基本概念は、次の 4 つです。

  • 依存オブジェクト (Dependency)
  • インジェクショントークン (Injection Token)
  • インジェクター (Injector)
  • プロバイダー (Provider)

まずは依存性の注入が使われる具体的な例を通じて、これらの概念がどのように登場するかを見てみましょう。

例. HttpClient を利用するコンポーネント

Angular の HttpClient を利用して HTTP リクエストを送信するコンポーネントを考えてみましょう。(あくまでも例なので、「コンポーネントで HTTP リクエストを扱うべきではない」ということは一旦忘れてください)
コンポーネントの httpClient フィールドでは、 HttpClient クラスをトークンとして指定して、inject()関数を使ってインスタンスを入手しています。そのインスタンスを提供しているのは provideHttpClient() 関数です。

@Component({...})
export class AppComponent {
    // `HttpClient` をトークンとしてインジェクターからインスタンスを入手
    httpClient = inject(HttpClient);
}

bootstrapApplication(AppComponent, {
    providers: [
        // `HttpClient` クラスのインスタンスを提供するプロバイダーを登録
        provideHttpClient()
    ]
});

ここでは 4 つの基本概念は次のように登場しています。

基本概念 登場する場所
依存オブジェクト HttpClient クラスのインスタンス
インジェクショントークン HttpClient クラス
インジェクター inject() 関数
プロバイダー provideHttpClient() 関数の戻り値

例. 自作のサービスを利用するコンポーネント

次に、自作のサービスを利用するコンポーネントを考えてみましょう。UserService として定義されたサービスを依存性の注入を通じて利用可能にするために、クラスには @Injectable({ providedIn: 'root' }) デコレータが付与されています。そして、コンポーネントの userService フィールドでは、 UserService クラスをトークンとして指定して、inject()関数を使ってインスタンスを入手しています。

// `UserService` のインスタンスを提供するプロバイダーを登録
@Injectable({ providedIn: 'root' })
export class UserService { }

@Component({...})
export class AppComponent {
    // `UserService` をトークンとしてインジェクターからインスタンスを入手
    userService = inject(UserService);
}

bootstrapApplication(AppComponent);

ここでは 4 つの基本概念は次のように登場しています。

基本概念 登場する場所
依存オブジェクト UserService クラスのインスタンス
インジェクショントークン UserService クラス
インジェクター inject() 関数
プロバイダー UserService クラスの @Injectable() デコレータ

例. 設定値を利用するコンポーネント

最後に、依存性の注入を通じてアプリケーションの設定値を利用するコンポーネントを考えてみましょう。アプリケーションのタイトル文字列を依存性の注入を通じて利用します。コンポーネントの appTitle フィールドでは APP_TITLE というトークンを指定して、inject()関数を使ってインスタンスを入手しています。このトークンに対して文字列を提供するプロバイダーは、アプリケーションの起動時に登録されています。

const APP_TITLE = new InjectionToken<string>('appTitle');

@Component({...})
export class AppComponent {
    // `APP_TITLE` をトークンとしてインジェクターからインスタンスを入手
    appTitle = inject(APP_TITLE);
}

bootstrapApplication(AppComponent, {
    providers: [
        // `APP_TITLE` トークンに対して文字列を提供するプロバイダーを登録
        { provide: APP_TITLE, useValue: 'My App' }
    ]
});

ここでは 4 つの基本概念は次のように登場しています。

基本概念 登場する場所
依存オブジェクト 'My App' 文字列
インジェクショントークン APP_TITLE
インジェクター inject() 関数
プロバイダー { provide: APP_TITLE, useValue: 'My App' }

Angular の依存性の注入においては常にこのような基本概念の関係構造が成り立っています。コンポーネントやサービスが外部のオブジェクトへの依存を持つとき、その依存オブジェクトをインジェクターから取り出すためのキーとなるのがインジェクショントークンです。そして、インジェクショントークンとオブジェクトを紐付けてインジェクターに登録するのがプロバイダーです。

依存性の注入の基本概念
依存性の注入の基本概念

この基本構造さえ覚えておけば、Angular の依存性の注入にかかわる API のそれぞれがどのような役割を果たしているのかを理解することができます。

プロバイダーの種類

プロバイダーはインジェクショントークンとオブジェクトを紐付ける方法によって 4 種類に分類されます。ですが、どのプロバイダーもすべて、最終的にやっていることは同じであることがわかってしまえば、それほど混乱することはないでしょう。

1. 値プロバイダー (Value Provider)

値プロバイダーは、インジェクショントークンに対して固定値を紐付けるプロバイダーです。インジェクターは値プロバイダーが提供した値をそのまま依存オブジェクトとして取り出します。実行時に確定する設定値など、アプリケーションの起動時に決まってしまう値を提供する場合に利用します。値プロバイダーは次の例のように useValue プロパティを使って値オブジェクトを指定します。

const APP_TITLE = new InjectionToken<string>('appTitle');

@Component({...})
export class AppComponent {
    // `APP_TITLE` をトークンとしてインジェクターから文字列値を入手
    appTitle = inject(APP_TITLE); // 'My App'
}

bootstrapApplication(AppComponent, {
  providers: [
    // `APP_TITLE` トークンに対して文字列値を提供する
    { provide: APP_TITLE, useValue: 'My App' },
  ],
});

2. ファクトリープロバイダー (Factory Provider)

ファクトリープロバイダーは、インジェクショントークンに対して関数を紐付けるプロバイダーです。インジェクターは関数を呼び出した戻り値を依存オブジェクトとして取り出します。ファクトリープロバイダーは次の例のように useFactory プロパティを使って関数オブジェクトを指定します。

const APP_TITLE = new InjectionToken<string>('appTitle');

@Component({...})
export class AppComponent {
    // `APP_TITLE` をトークンとしてインジェクターから文字列値を入手
    appTitle = inject(APP_TITLE); // 'My App'
}

bootstrapApplication(AppComponent, {
  providers: [
    // `APP_TITLE` トークンに対して文字列値を提供する
    { provide: APP_TITLE, useFactory: () => 'My App' },
  ],
});

3. クラスプロバイダー (Class Provider)

クラスプロバイダーは、インジェクショントークンに対してクラスを紐付けるプロバイダーです。インジェクターはクラスのコンストラクターを呼び出してインスタンスを生成し、そのインスタンスを依存オブジェクトとして取り出します。クラスプロバイダーは次の例のように useClass プロパティを使ってクラスオブジェクトを指定します。

@Injectable()
class AppTitleService {
    title = 'My App';
}

@Component({...})
export class AppComponent {
    // `AppTitleService` をトークンとしてインジェクターからクラスインスタンスを入手
    appTitleService = inject(AppTitleService); // AppTitleService
}

bootstrapApplication(AppComponent, {
    providers: [
        // `AppTitleService` トークンに対して
        // `AppTitleService` クラスのインスタンスを提供する
        { provide: AppTitleService, useClass: AppTitleService },
    ],
});

インジェクショントークンとクラスが一致する場合は、次のように省略して書くこともできます。

bootstrapApplication(AppComponent, {
  providers: [
    // クラスプロバイダーの省略記法
    AppTitleService,
  ],
});

インジェクショントークンと提供するクラスが必ずしも一致している必要はありません。たとえば、次の例のように抽象クラスをインジェクショントークンとして使い、具象クラスのインスタンスが提供するようなこともできます。

@Injectable()
abstract class AppTitleService {
    abstract getTitle(): string;
}

@Injectable()
class AppTitleServiceImpl extends AppTitleService {
    getTitle() {
        return 'My App';
    }
}

@Component({...})
export class AppComponent {
    // `AppTitleService` をトークンとしてインジェクターからクラスインスタンスを入手
    appTitleService = inject(AppTitleService); // AppTitleServiceImpl
}

bootstrapApplication(AppComponent, {
    providers: [
        // `AppTitleService` トークンに対して `AppTitleServiceImpl` クラスのインスタンスを提供する
        { provide: AppTitleService, useClass: AppTitleServiceImpl },
    ],
});

4. エイリアスプロバイダー (Alias Provider)

エイリアスプロバイダーは、インジェクショントークンに対して別のプロバイダーを紐付けるプロバイダーです。エイリアスプロバイダーは次の例のように useExisting プロパティを使って別のインジェクショントークンを指定します。インジェクターは複数のインジェクショントークンから同一のオブジェクトを取り出すことができます。

const APP_TITLE_1 = new InjectionToken<string>('appTitle1');
const APP_TITLE_2 = new InjectionToken<string>('appTitle2');

@Component({...})
export class AppComponent {
    // `APP_TITLE` をトークンとしてインジェクターから文字列値を入手
    appTitle = inject(APP_TITLE2); // 'My App'
}

bootstrapApplication(AppComponent, {
    providers: [
        { provide: APP_TITLE_1, useValue: 'My App' },
        // `APP_TITLE_2` トークンに対して `APP_TITLE_1` へのエイリアスを設定する
        { provide: APP_TITLE_2, useExisting: APP_TITLE_1 },
    ],
});

これらのプロバイダーはオブジェクトを生成する方法が違うだけで、依存性の注入における役割は同じです。用途や目的に応じて開発者が適切なプロバイダーを選択することができます。

provideXXX 関数パターン

Angular の組み込み API やそれを真似したライブラリでは、プロバイダーを返す関数に provide 接頭辞をつける慣習がありますが、慣習的であるという以上の意味はありません。たとえば、provideHttpClient() 関数は、実際には HttpClient クラスのインスタンスを提供するプロバイダーを返しているだけです。 (ソースコード)

provideHttpClient関数の中身
export function provideHttpClient(...features: HttpFeature<HttpFeatureKind>[]): EnvironmentProviders {
    // ...
    const providers: Provider[] = [
        HttpClient,
        // ...
    ];
    // ...
    return makeEnvironmentProviders(providers);
}

慣習はあくまでも慣習ですが、同じような名前の関数が同じような役割を持っているということは、開発者にとっては便利なことです。はじめて使うライブラリでも使い方に迷いませんし、新しくチームに加わった開発者がコードを読むときにも理解を助けてくれます。プロバイダーを返す関数を作る場合は、できるだけ provide 接頭辞をつけることをおすすめします。

インジェクターの種類

プロバイダーに種類があったように、インジェクターにもいくつかの種類があります。どのインジェクターも基本的な役割は変わらず、インジェクショントークンをキーにしてオブジェクトを取り出します。インジェクターの種類は、そのインジェクターのライフサイクルの違いによって区別されます。

環境インジェクター (Environment Injector)

環境インジェクターは、プロバイダーから提供されたオブジェクトをアプリケーションの実行環境レベルで保持するインジェクターです。一般的に、このインジェクターはアプリケーションの起動時にひとつだけ作成され、アプリケーションが破棄されるまで存在します。

次の方法で設定されているプロバイダーは、すべて環境インジェクターに登録されます。

  • bootstrapApplication() 関数のオプションが持つ providers プロパティ
  • @Injectable({ providedIn: 'root' }) デコレータ
  • Route.providers プロパティ

環境インジェクターはその名のとおり、環境変数のようにアプリケーションのどこからでも利用できます。アプリケーションのなかで一貫して同じインスタンスを使いたい(いわゆるシングルトンな)オブジェクトは、環境インジェクターに登録することになるでしょう。

ノードインジェクター (Node Injector)

環境インジェクターに対してノードインジェクターは、コンポーネントやディレクティブのライフサイクルで保持されるインジェクターです。ノードインジェクターは、そのノードとともに生成され、ノードが破棄されると同時に破棄されます。コンポーネントであれば、コンポーネントの初期化とともに作成され、コンポーネントの破棄とともに破棄されます。

@Component({
  // コンポーネントインスタンスごとに作成されるノードインジェクターに対するプロバイダー
  providers: [LocalService],
})
class AppComponent {}

コンポーネントとライフサイクルを一致させたいオブジェクトや、コンポーネントインスタンスごとに独立したインスタンスを保持したいサービスなどは、ノードインジェクターに登録することになるでしょう。

ノードインジェクターと環境インジェクターは階層化されているため、あるインジェクショントークンに紐付いたプロバイダーがノードインジェクターに登録されていない場合は、その親ノード(コンポーネント)のノードインジェクターへさかのぼり、そこにもプロバイダーがない場合は、さらに親へとさかのぼります。そして最終的には環境インジェクターにたどり着きます。環境インジェクターにも該当するプロバイダーがない場合は、エラーが発生します。(NG0201: No provider for {token} found!)

inject() 関数

inject() 関数は、インジェクショントークンを引数に受け取り、呼び出しコンテキストに応じてもっとも適切なインジェクターからオブジェクトを取り出します。ただし、依存性の注入が可能なコンテキスト (injection context) は限られており、そのコンテキストの外で inject() 関数を呼び出すことはできません。たとえば、コンポーネントのフィールド初期化時やコンストラクタ内では inject() 関数を呼び出すことができますが、ngOnInit() メソッドなどそれ以外のコンテキストでは呼び出すことができず、エラーが発生します。(NG0203: `inject()` must be called from an injection context)

@Component({...})
export class AppComponent {
    // 呼び出せる
    appTitle = inject(APP_TITLE);

    ngOnInit() {
        // 呼び出せない
        this.appTitle = inject(APP_TITLE);
    }
}

また、inject() 関数の戻り値の型は渡されたインジェクショントークンから推論されます。クラスオブジェクトを渡した場合はそのインスタンスの型になり、InjectionToken<T> 型であった場合には、その型パラメータ T が返り値の型となります。文字列をインジェクショントークンとして渡した場合は、any 型になってしまうため、ファクトリープロバイダーや値プロバイダーを使う場合は、InjectionToken 型を使うことが推奨されています。

const VALUE_TOKEN = new InjectionToken<string>('value');

inject(VALUE_TOKEN); // => string 型
inject('VALUE_TOKEN'); // => any 型

この章のまとめ

この章では Angular における依存性の注入の基本構造を解説しました。プロバイダーやインジェクターにはいくつかのバリエーションがありますが、どれも根本的な役割は同じです。このことを念頭におけばどんな Angular アプリケーションのコードでも、依存性の注入がどのように使われているかを把握できるでしょう。

また、これから Angular を深く学んでいく上で、依存性の注入への理解は前提知識として求められます。多くの実践的な機能が依存性の注入をベースに実装されているため、それらの間違った使い方をしないためにも、この章の内容がわからなくなったら何度でも復習することをおすすめします。

参考リンク