LoginSignup
10
4

More than 5 years have passed since last update.

Angular2 DOC GUIDEを翻訳する[DEPENDENCY INJECTION]

Last updated at Posted at 2016-11-22

トピック

Angular2 CORE DOCUMENTATIONのGUIDEの翻訳です。

注意1)ここに掲載されていない項目は、Angular2 CORE DOCUMENTATIONのGUIDEを直接参照してください。

注意2)2016年11月22日時点の翻訳です。翻訳者はTOEICで700点くらいの英語力なので、英訳が間違っている可能性があります。しかもかなり意訳している箇所もあります。もし意訳を通り越して、誤訳になっているような箇所がありましたらご指摘ください。

DEPENDENCY INJECTION - 依存性注入 -

Angularの依存性注入(DI)は、"必要なときに、必要なだけ"依存するサービスを作り出し、運用するシステムです。

依存性注入は、重要なアプリケーションデザインパターンです。Angularには独自の依存性注入フレームワークがあり、Angularのアプリケーションはそれなしに構築できません。そしてそれは多くの人にDIと呼ばれ、広く使われています。

この章では、DIとはどんなものか、なぜDIを使うのかといったことを学習します。そのあと、実際にAngularアプリの中での使用方法について学んでいきます。

live exampleを起動してみてください。

なぜ依存性注入なのか

では早速、次のコードを使って始めてみましょう。

app/car/car.ts(without_DI)
export class Car {
  public engine: Engine;
  public tires: Tires;
  public description = 'No DI';
  constructor() {
    this.engine = new Engine();
    this.tires = new Tires();
  }
  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` +
      `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  }
}

Carはコンストラクタの中で必要なものをすべて作り出しています。何が問題なのでしょうか。問題は、Carクラスが脆弱で柔軟性がなく、テストが難しくなっているところにあります。

Carにはエンジンとタイヤが必要です。その本体を要求する代わりに、Carのコンストラクタが特殊なクラスであるEngineTiresからそのコピー(インスタンス)を作成しています。

もしEngineクラスが更新され、そのコンストラクタがパラメータを要求してきたらどうなるでしょうか。
Carは壊れてしまい、this.engine = new Engine(theNewParameter)と行を書き直すまで壊れたままになります。もし初めてCarを書いたというのなら、Engineにかかるコンストラクタのパラメータがなんであっても構いません。なので、今のところは何も気にする必要はないのですが、Engineの定義が変更され、Carを変更しなければならなくったときのために、あらかじめ気にしておかなければならないのです。それがCarの脆弱性へとつながります。

では、もし違うブランドのタイヤをCarに取り付けたくなったとしたらどうでしょうか。最悪です。Tiresクラスが作ったブランドの場合、どんなものであってもそのブランドしか選べなくなります。それがCar の柔軟性のなさへとつながります。

今すぐ新しい車に、独自のエンジンを取り付けてみましょう。エンジンをほかの車と共有することはできません。車のエンジンはそれで問題ありませんが、メーカーのサービスセンターへつなぐ車内無線のような、共有されるべきほかの依存性についてはどうでしょうか。Carは、あらかじめほかの消費者のために作られたサービスを共有するといった柔軟性を持ち合わせていません。

Carのテストを書こうとすると、その隠れた依存性に翻弄されることになります。テスト環境で、新しいEngineを作ることは可能でしょうか。Engineそのものが依存しているものは何でしょうか。その依存しているものは、何に依存しているのでしょうか。Engineの新しいインスタンスは、サーバーへ非同期通信を行うのでしょうか。テスト中、ずっとそんなことをやっているなんてどんでもないです。

タイヤの空気圧が少なくなったとき、Carは警告信号を発すべきでしょうか。テスト中に空気圧が少ないタイヤに交換できないとなったら、実際に発せられる信号をどうやって確認すればよいのでしょうか。

その車の隠れた依存性をコントロールする術はありません。依存性の管理ができないとなると、クラスのテストは難しくなります。

では、どうすればCarをより強固で、柔軟性のある、テストしやすいものにできるのでしょうか?

その答えは超簡単です。Carのコンストラクタを、DIを使ったバージョンに変えればいいのです。

app/car/car.ts(excerpt_with_DI)
public description = 'DI';

constructor(public engine: Engine, public tires: Tires) { }
app/car/car.ts(excerpt_without_DI)
public engine: Engine;
public tires: Tires;
public description = 'No DI';

constructor() {
  this.engine = new Engine();
  this.tires = new Tires();
}

何が起こったか見てみましょう。私たちは依存性の定義をコンストラクタに移しました。Carクラスはもうエンジンもタイヤも作りません。ただ、それを消費するだけです。

パラメータとプロパティを同時に宣言する、TypeScriptのコンストラクタシンタックスも利用しています。

では、エンジンとタイヤをコンストラクタに移して車を作ります。

// Simple car with 4 cylinders and Flintstone tires.
let car = new Car(new Engine(), new Tires());

すごくクールじゃないですか?エンジンやタイヤの依存性の定義は、Carクラスそのものからは分離されています。どんな種類のエンジンやタイヤを好きに選んだとしても、エンジンやタイヤの汎用的なAPI要件さえ確認しておけば使用可能です。

誰かがEngineクラスを拡張したとしても、Carの問題にはなりません。

Carの購買者には問題が生じます。購買者は次のような形で車の制作コードを更新しなければなりません。

class Engine2 {
  constructor(public cylinders: number) { }
}
// Super car with 12 cylinders and Flintstone tires.
let bigCylinders = 12;
let car = new Car(new Engine2(bigCylinders), new Tires());

大事なのは、Carそのものを変更する必要がない、ということです。購買者の問題は、起こってからすぐに対応すればいいのです。

依存性が完全にコントロールされているので、Carクラスはテストがかなり簡単になります。テスト中に私たちがやってもらいたいことは、コンストラクタにモックを渡せば確実にやってくれます。

class MockEngine extends Engine { cylinders = 8; }
class MockTires  extends Tires  { make = 'YokoGoodStone'; }

// Test car with 8 cylinders and YokoGoodStone tires.
let car = new Car(new MockEngine(), new MockTires());

DIとは何なのかについて学びました。

それは依存性自体を作るというより、クラスが外部のソースから依存性を受け取るといったイメージのコーディングパターンです。

かっこいいですね!ですが、購買者が貧しかったらどうでしょうか。Carをほしい人はみんな、CarEngineTiresという3つの部品すべてを作らないといけなくなりました。Carクラスは購買者が負担するという問題を抱えています。なので、これらの部品の組み立てに配慮してくれる何かが必要となります。

組み立てをやってくれる巨大なクラスを書いてみました。

app/car/car-factory.ts
import { Engine, Tires, Car } from './car';
// BAD pattern!
export class CarFactory {
  createCar() {
    let car = new Car(this.createEngine(), this.createTires());
    car.description = 'Factory';
    return car;
  }
  createEngine() {
    return new Engine();
  }
  createTires() {
    return new Tires();
  }
}

たった3つの作成用メソッドなのでそれほど悪くないような気がしますが、これではアプリケーションが拡大していくにしたがって、メンテナンスが困難になってしまいます。このファクトリーは、独立したファクトリーメソッドだらけの、巨大な蜘蛛の巣のようになっていくでしょうね。

では、どの依存性を何に注入するのかを決める必要がないような場合、作りたいものを単純に並べるようなやり方はよくないでしょうか?

これはDIフレームワークがうまくやってくれます。このフレームワークには、インジェクタを呼び出すものがあるとイメージしてください。そのインジェクタをもったクラスを設定してみれば、それらがどのようにして作られるかがわかります。

Carが必要なときは、単純にそれを取得するためのインジェクタを呼び出せば上手くいきます。

let car = injector.get(Car);

これですべてが上手くいきました。CarEngineTiresを作ることについて何も知りませんし、購買者はCarの作り方について何もわかっていません。メンテナンスが必要な巨大なファクトリークラスなんて必要ないのです。Carと購買者はとにかく必要なものだけを要求しておけば、あとはインジェクタがうまくやってくれます。

DIフレームワークとはこういうものです。

さて、DIとは何なのかがわかりましたので、その恩恵に感謝しつつ、Angularでの使いかたを見ていきましょう。

Angularの依存性注入

Angularは独自のDIフレームワーク持っています。このフレームワークは、異なるアプリケーションやフレームワークで、独立したモジュールとして使用することもできます。

よさそうな感じですが、Angularのコンポーネントを構築するときには何をすればよいのでしょうか。
1つずつ見ていきましょう。

The Tour of Heroesで構築したHeroesComponentの簡易版から始めてみます。

app/heroes/heroes.component.ts
import { Component } from '@angular/core';
@Component({
  selector: 'my-heroes',
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `
})
export class HeroesComponent { }
app/heroes/hero-list.component.ts
import { Component }   from '@angular/core';
import { HEROES }      from './mock-heroes';
@Component({
  selector: 'hero-list',
  template: `
  <div *ngFor="let hero of heroes">
    {{hero.id}} - {{hero.name}}
  </div>
  `
})
export class HeroListComponent {
  heroes = HEROES;
}
app/heroes/hero.ts
export class Hero {
  id: number;
  name: string;
  isSecret = false;
}
app/heroes/mock-heroes.ts
import { Hero } from './hero';
export var HEROES: Hero[] = [
  { id: 11, isSecret: false, name: 'Mr. Nice' },
  { id: 12, isSecret: false, name: 'Narco' },
  { id: 13, isSecret: false, name: 'Bombasto' },
  { id: 14, isSecret: false, name: 'Celeritas' },
  { id: 15, isSecret: false, name: 'Magneta' },
  { id: 16, isSecret: false, name: 'RubberMan' },
  { id: 17, isSecret: false, name: 'Dynama' },
  { id: 18, isSecret: true,  name: 'Dr IQ' },
  { id: 19, isSecret: true,  name: 'Magma' },
  { id: 20, isSecret: true,  name: 'Tornado' }
];

HeroesComponentHeroesの機能領域におけるルートコンポーネントです。この領域にあるすべての子コンポーネントを制御しています。この縮小版では、HeroListComponentというヒーローのリストを表示する子を1つだけにしました。

早速HeroListComponentで、ほかのファイルで定義されているHEROESというメモリ上のコレクションからヒーローを取得してみます。開発の早い段階ではそれで十分かもしれませんが、理想はだいぶ遠いところにあります。できるだけ早くこのコンポーネントでテストを試し、リモートサーバからヒーローのデータを取得したいところです。それが終わったら、heroesの実装を変更し、HEROESモックデータが使われているすべての箇所を修正する必要があります。

では、ヒーローデータを取得しているところを隠すサービスを作ってみましょう。

サービスはseparate concern(独立した関係)であるべきなので、サービスのコードは独自のファイルに書くことをお勧めします。

この記述で詳細を確認してください。

app/heroes/hero.service.ts
import { Injectable } from '@angular/core';
import { HEROES }     from './mock-heroes';
@Injectable()
export class HeroService {
  getHeroes() { return HEROES;  }
}

HeroServiceは事前に用意したモックと同じデータを返すgetHeroesを設定していますが、それを知る購買者はいません。

上のサービスクラスにある@Injectable()デコレータに注意してください。その目的について、簡単に解説しています。


これが本当のサービスであるととぼけるつもりはありません。もし実際にリモートサーバからデータを得ようとするなら、APIは非同期とする必要があり、おそらくPromiseを返すことになったでしょう。また、コンポーネントがサービスを使えるように書き換える必要もあります。これも大事なことなのですが、この話の中ではいったん置いておきます。

サービスはAngularのクラス以外の何物でもありません。Angularのインジェクタを使って登録するまで、1つのクラスがあるだけという状態になっています。

インジェクタを設定する

Angularのインジェクタをわざわざ作る必要はありません。Angularが起動する過程の中で、アプリケーションを範囲としたインジェクタが作成されます。

app/main.ts(excerpt)
platformBrowserDynamic().bootstrapModule(AppModule);

アプリケーションが要求するサービスを作るprovidersを登録することで、インジェクタを設定する必要があります。providersがどんなものなのかは、この章の後半で説明します。

また、プロバイダはNgModuleやアプリケーションコンポーネントにも登録することができます。

NgModuleにprovidersを登録する

ここでAppModuleにLoggerUserServiceAPP_CONFIGプロバイダを登録します。

app/app.module.ts
@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent,
    CarComponent,
    HeroesComponent,
    HeroListComponent,
    InjectorComponent,
    TestComponent,
    ProvidersComponent,
    Provider1Component,
    Provider3Component,
    Provider4Component,
    Provider5Component,
    Provider6aComponent,
    Provider6bComponent,
    Provider7Component,
    Provider8Component,
    Provider9Component,
    Provider10Component,
  ],
  providers: [
    UserService,
    { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
  ],
  bootstrap: [ AppComponent, ProvidersComponent ]
})
export class AppModule { }

コンポーネントにprovidersを登録する

ここではHeroServiceHeroesComponentに登録するよう修正を加えます。

app/heroes/heroes.component.ts
import { Component } from '@angular/core';

import { HeroService } from './hero.service';

@Component({
  selector: 'my-heroes',
  providers: [HeroService],
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `
})
export class HeroesComponent { }

NgModuleとアプリケーションコンポーネントを使うタイミングは?

NgModuleのプロバイダはルートインジェクタの中に登録されます。NgModuleにあるすべてのプロバイダが登録され、アプリケーション全体からアクセスできるようになります。

一方、アプリケーションコンポーネントで登録されたプロバイダは、そのコンポーネントと、すべての子コンポーネントからのみ利用が可能になります。

APP_CONFIGサービスはアプリケーションのどこからでもアクセスできるようにしておきたいですが、HeroServiceHeroes機能の範囲に留め、それ以外からは使われないようにしておきます。

NgModule FAQの章にあるアプリを範囲としたprovidersをルートのAppModuleに加えるべきか、AppComponentに加えるべきかも読んでおいてください。

HeroListComponentのインジェクションを用意する

HeroListComponentは注入されたHeroServiceからheroesを取得すべきです。DIパターンごとで、前に説明したようにコンポーネントはコンストラクタにあるサービスを要求しなければなりません。ちょっとした変更です。

app/heroes/hero-list.component(with_DI)
import { Component }   from '@angular/core';
import { Hero }        from './hero';
import { HeroService } from './hero.service';
@Component({
  selector: 'hero-list',
  template: `
  <div *ngFor="let hero of heroes">
    {{hero.id}} - {{hero.name}}
  </div>
  `
})
export class HeroListComponent {
  heroes: Hero[];
  constructor(heroService: HeroService) {
    this.heroes = heroService.getHeroes();
  }
}
app/heroes/hero-list.component(without_DI)
import { Component }   from '@angular/core';
import { HEROES }      from './mock-heroes';
@Component({
  selector: 'hero-list',
  template: `
  <div *ngFor="let hero of heroes">
    {{hero.id}} - {{hero.name}}
  </div>
  `
})
export class HeroListComponent {
  heroes = HEROES;
}

FOCUS ON THE CONSTRUCTOR

コンストラクタにパラメータを加える場合、すべてが下記のようになるわけではありません。

constructor(heroService: HeroService) {
  this.heroes = heroService.getHeroes();
}

コンストラクタのパラメータがHeroServiceの型を持っていて、HeroListComponentクラスが@Componentデコレータ(スクロールしてそのことを確認してください)を持っていることに注目してください。また、親コンポーネント(HeroesComponent)はHeroServiceproviders情報を持っていることも思い出してください。

新しいHeroListComponentが作られるとき、コンストラクタのパラメータの型、@Componentデコレータ、そして親コンポーネントのprovidersの情報は、AngularのインジェクタにHeroServiceのインスタンスを注入するよう伝えるため結合されます。

暗黙的にインジェクタを作成する

上記ではインジェクタという概念を紹介する際、新しくCarを作ることでその使い方をお見せしました。ここではインジェクタを明示的に作成する方法を示します。

  injector = ReflectiveInjector.resolveAndCreate([Car, Engine, Tires]);
  let car = injector.get(Car);

Tour of Heroesや他のサンプルでは、このようなコードは見当たりません。必要があれば明示的にインジェクタを作るコードを書くこともできますが、そんなことはほとんどありません。Angularがインジェクタを作成し、呼び出しにかかるのは、<hero-list></hero-list>のようなHTMLマークアップを使う、もしくはルータを使ってコンポーネントに移動した後、コンポーネントが作成されるときです。Angularにその作業をさせておけば、自動化されたDIの利便性を享受することになります。

シングルトンサービス

依存性とは、インジェクタのスコープにあるシングルトン(単体)です。サンプルでは、1つのHeroServiceインスタンスがHeroesComponentとその子であるHeroListComponentに共有されています。

しかし、AngularのDIは階層的に注入していくシステムなので、入れ子になったインジェクタはそれぞれ独自のサービスのインスタンスを作成します。詳しくはHierarchical Injectorsの章で学習してください。

コンポーネントをテストする

DI用のクラスを設計する場合、クラスをより簡単にテストすることを以前に強調しました。依存関係をコンストラクタのパラメータとして併記していけば、アプリケーションの部品を効率的にテストすることができます。

たとえば、テスト中でも操作可能なモックのサービスを使って、新しくHeroListComponentを作ることができます。

let expectedHeroes = [{name: 'A'}, {name: 'B'}]
let mockService = <HeroService> {getHeroes: () => expectedHeroes }

it('should have heroes when HeroListComponent created', () => {
  let hlc = new HeroListComponent(mockService);
  expect(hlc.heroes.length).toEqual(expectedHeroes.length);
});

詳しくはTestingで学習してください。

サービスがサービスを必要とするとき

HeroServiceはとてもシンプルです。それ自体は何の依存性も持っていません。

では、もしサービスが依存性を持っていたらどうでしょうか。その行動をログサービスを通して記録していた場合は?Loggerパラメータを持ったコンストラクタを加え、同じようにコンストラクタインジェクションパターンを適用してみましょう。

オリジナルと比較できる修正版を用意しました。

app/heroes/hero.service(v2)
import { Injectable } from '@angular/core';
import { HEROES }     from './mock-heroes';
import { Logger }     from '../logger.service';
@Injectable()
export class HeroService {
  constructor(private logger: Logger) {  }
  getHeroes() {
    this.logger.log('Getting heroes ...');
    return HEROES;
  }
}
app/heroes/hero.service(v1)
import { Injectable } from '@angular/core';
import { HEROES }     from './mock-heroes';
@Injectable()
export class HeroService {
  getHeroes() { return HEROES;  }
}

コンストラクタは注入されたLoggerのインスタンスを要求し、loggerというプライベートプロパティに保存します。ヒーローが誰かに求められると、getHeroesメソッドの中にあるプロパティが呼び出されます。

なぜ@Injectable()なのか

@Injectable()は、インスタンス化したインジェクタを利用するクラスにマークします。一般的に言えば、インジェクタは@Injectable()でマークされていないクラスをインスタンス化しようとすると、エラーを表示します。

HeroServiceの最初のバージョンでクラスをインスタンス化しようとしたとき、HeroServiceにはパラメータが注入されていなかったので@Injectable()を省略することもできました。しかし、新しいバージョンではサービスにDIがあるので、@Injectable()を省略することはできません。Angularがコンストラクタパラメータのメタデータを要求するので、Loggerを注入するためには@Injectable()が必要なのです。

SUGGESTION: ADD @INJECTABLE() TO EVERY SERVICE CLASS

@Injectable()はたとえ依存関係を持っておらず、それによって技術的な要求をされていなかったとしても、すべてのサービスクラスに追加しておくことをお勧めします。理由は次のとおりです。

  • 将来的な補強: 後で依存性を加えようとした時に、@Injectable()を思い出す必要がありません。
  • 一貫性: すべてのサービスが同じルールに従えば、なぜデコレータがないのかと思いを巡らすこともありません。

インジェクタはまたHeroesComponentのようなコンポーネントのインスタンス化を担保します。なぜ@Injectable()HeroesComponentにマークしなかったのでしょうか。

もし本当にそれが必要なら、@Injectable()加えることもできます。しかし、HeroesComponentはすでに@Componentでマークされており、このデコレータクラス(@Directive@Pipeは後ほど学習します)はInjectableのサブタイプなので、必要性がないのです。事実、Injectableデコレータはインジェクタによって、インスタンス化のターゲットとなるクラスを特定します。

アプリを起動するとき、インジェクタはトランスコンパイルされたJavaScriptのコードの中でクラスのメタデータを読み込み、注入するものを決めるため、コンストラクタのパラメータ型の情報を使うことができます。

すべてのJavaScriptクラスがメタデータを持っているわけではありません。TypeScriptコンパイラは、デフォルトではメタデータを捨ててしまいます。emitDecoratorMetadataというコンパイラオプションがtrueである(それはtsconfig.jsonの中に記載されているはず)なら、コンパイラはメタデータが生成されたJavaScriptのすべてのクラスに、少なくとも1つのデコレータを加えます。

デコレータがこの効果の引き金となっている間は、サービスクラスの内部をきれいにするため、Injectableデコレータをマークするようにしてください。

ALWAYS INCLUDE THE PARENTHESES

@Injectableだけではなく、常に@Injectable()と書いてください。もしこの丸括弧を忘れたりすると、アプリケーションがなぜか勝手に落ちたりします。

loggerサービスを作成し、登録する

HeroServiceの中に、2つの手順を踏んでloggerを注入してみます。

  1. loggerサービスを作成します。
  2. アプリケーションにそれを登録します。

loggerサービスは極めてシンプルです。

app/logger.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class Logger {
  logs: string[] = []; // capture logs for testing
  log(message: string) {
    this.logs.push(message);
    console.log(message);
  }
}

このアプリケーションでは、すべての場所に同じloggerサービスがある必要がありそうなので、プロジェクトのappフォルダにそれを設置し、アプリケーションモジュールであるAppModuleに、メタデータのproviders配列として登録しておきます。

app/app.module.ts_(excerpt)_(providers-logger)
providers: [Logger]

loggerの登録を忘れると、Angularは最初にloggerを見つけた時に例外を投げてしまいます。

EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger)

DIがloggerというプロバイダを見つけられなかったと、Angularが教えてくれます。loggerをプロバイダに登録することは、プロバイダがLoggerを作って新しいHeroServiceに注入するために必要なことであり、またそれは新しいHeroListComponentに作成、注入するために必要なことでもあります。

生成の連鎖は、Loggerプロバイダから始まります。Providersが、次のセクションの主題です。

インジェクタプロバイダ

プロバイダは、依存関係にかかる値の具体的な実行時バージョンを提供します。インジェクタはサービスのインスタンスを作るとき、providersを使ってコンポーネントやほかのサービスにそのサービスを注入しています。

インジェクタを使ってサービスプロバイダを登録しなければ、サービスをどのように作成してよいのかわかりません。

前にAppModuleのメタデータであるprovidersの配列に、Loggerサービスを次のようにして登録しました。

providers: [Logger]

見た目と振舞いがLoggerのようなものを提供する方法はたくさんあります。Loggerクラス自体はわかりやすい純粋なプロバイダです。ただ、やり方が1つではありません。

providersという代替可能な手段を使ってインジェクタを設定し、Loggerのような振舞いをするオブジェクトを実行することもできますし、代わりとなるクラスを提供することもできます。また、loggerのようなオブジェクトの提供もできれば、loggerファクトリーファンクションを呼び出すプロバイダを付与することもできます。問題の生じない状況下であれば、こういったすべてのアプローチが正しい選択となりうるのです。

Loggerが必要になったとき、インジェクタに実行できるプロバイダがあることが重要なのです。

プロバイダクラスとプロバイダオブジェクトリテラル

これまではprovidersの配列を次のように書いていました。

providers: [Logger]

これは実際のところ、2つのプロパティを持つproviderオブジェクトリテラルを使った、プロバイダ登録用の省略した表現となっています。

[{ provide: Logger, useClass: Logger }]

1つ目の値は、依存関係にかかる値の特定とプロバイダの登録の両方のキーとなるトークンです。

2つ目の値は、プロバイダの定義オブジェクトです。依存関係にかかる値を作成するためのレシピと考えることができます。 依存関係の値を作成するには多くの方法があり、また、レシピを書く場合にも多彩な方法があります。

代替クラスプロバイダ

サービスを提供するために、別のクラスを要求することがあります。次のコードは、何かが Loggerを要求したときにBetterLoggerを返すようインジェクタに指示しています。

[{ provide: Logger, useClass: BetterLogger }]

依存性を持ったクラスプロバイダ

EvenBetterLoggerなら、ログメッセージにユーザ名を表示できるかもしれません。 このロガーは、注入された UserServiceからユーザーを取得します。これはアプリケーションレベルでも注入されます。

@Injectable()
class EvenBetterLogger extends Logger {
  constructor(private userService: UserService) { super(); }

  log(message: string) {
    let name = this.userService.user.name;
    super.log(`Message to ${name}: ${message}`);
  }
}

BetterLoggerのように設定してください。

[ UserService,
  { provide: Logger, useClass: EvenBetterLogger }]

エイリアスクラスプロバイダ

古いコンポーネントが OldLoggerクラスに依存しているとします。 OldLoggerNewLoggerと同じインターフェースを持っていますが、何らかの理由でそれを使用するために古いコンポーネントを更新することはできません。

古いコンポーネントが OldLoggerでメッセージを記録するとき、OldLoggerではなくNewLoggerのシングルトンインスタンスで処理することが望ましいです。

依存関係のあるインジェクタは、コンポーネントが新しいロガーまたは古いロガーのどちらかを要求するときに、そのシングルトン・インスタンスを挿入する必要があります。 OldLoggerNewLoggerのエイリアスでなければなりません。

このアプリでは2つの異なる NewLoggerインスタンスを必要としないのですが、残念なことにuseClassOldLoggernewLoggerに エイリアスしようとすると、それが得られてしまいます。

[ NewLogger,
  // Not aliased! Creates two instances of `NewLogger`
  { provide: OldLogger, useClass: NewLogger}]

解決策は、useExistingオプションによるエイリアスです。

[ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}]

バリュープロバイダ

既製のオブジェクトを提供する方が、インジェクタにクラスから作成するよう要求するより簡単な場合があります。

// An object in the shape of the logger service
let silentLogger = {
  logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
  log: () => {}
};

次に、プロバイダーを useValueオプションで登録します。これにより、このオブジェクトはロガーの役割を果たします。

[{ provide: Logger, useValue: silentLogger }]

非クラスの依存関係OpaqueTokenのセクションにあるuseValueの例を参照してください。

ファクトリープロバイダ

最後までベースとなる情報が得られず、依存関係の値を動的に作り出さないといけないことがあります。可能性として、ブラウザセッションの過程で情報が繰り返し変更されているような場合があげられます。

注入可能なサービスが、この情報源に独自でアクセスできないと仮定してみます。

この状況下では、ファクトリープロバイダを呼び出します。

新しいビジネス要件を追加することで説明しましょう:HeroServiceは、秘密のヒーローを通常のユーザーから隠す必要があります。 許可されたユーザーだけが秘密のヒーローを確認できます。

EvenBetterLoggerのように、HeroServiceはユーザに関する情報を必要とします。 そのユーザーに秘密のヒーローを確認する権限があるかどうかを知らないといけません。 その権限は、異なるユーザーにログインするときと同様、単一のアプリケーションにセッションしている間に変更することができます。

ただEvenBetterLoggerと違うのは、HeroServiceUserServiceを注入することができないということです。HeroServiceは許可された人とされていない人を判断しようとして、ユーザ情報へ直接アクセスするということができません。

なんででしょうか。わかりません。こうなります。

その代わり、HeroServiceコンストラクタは、秘密のヒーローの表示を制御するブール値のフラグを持ちます。

app/heroes/hero.service.ts(excerpt)
constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

Loggerを注入することはできますが、isAuthorizedのブール値を注入することはできません。
この場合、ファクトリープロバイダを使って新しくHeroServiceのインスタンスを作り、引き継いでおく必要があります。

ファクトリープロバイダには、ファクトリーファンクションが必要です。

app/heroes/hero.service.provider.ts(excerpt)
let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

HeroServiceだとUserServiceにアクセスする方法がありませんが、ファクトリーファンクションならアクセス可能です。

LoggerUserServiceの両方をファクトリープロバイダに注入し、ファクトリーファンクションを使ってそれらをインジェクタに渡します。

app/heroes/hero.service.provider.ts(excerpt)
export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

useFactoryフィールドは、プロバイダがheroServiceFactoryを実装したファクトリーファンクションであることをAngularに伝えます。

depsプロパティはプロバイダトークンの配列です。LoggerUserServiceのクラスは独自のプロバイダクラスプロバイダを持つトークンとして保存されます。インジェクタはこれらのトークンを解決し、ファクトリーファンクションのパラメータと照合して一致するサービスに注入を行います。

ファクトリープロバイダをエクスポートされた変数heroServiceProviderで取得したことに注目してください。 この追加の手順によって、ファクトリープロバイダが再利用可能になります。 必要があれば、この変数を使ってどこからでもHeroServiceを登録することができます。

サンプルを見返してみると、メタデータprovidersの配列にあるHeroServiceを置き換える必要があるのはHeroesComponentだけです。次の例では、新しい実装と古い実装を並べて表示しています。

app/heroes/heroes.component_(v3)
import { Component }          from '@angular/core';
import { heroServiceProvider } from './hero.service.provider';
@Component({
  selector: 'my-heroes',
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `,
  providers: [heroServiceProvider]
})
export class HeroesComponent { }
app/heroes/heroes.component(v2)
import { Component }          from '@angular/core';
import { HeroService }        from './hero.service';
@Component({
  selector: 'my-heroes',
  providers: [HeroService],
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `
})
export class HeroesComponent { }

DIトークン

インジェクタを使ってプロバイダを登録すると、そのプロバイダはDIトークンと関連付けられます。
インジェクタは依存関係を要求されたときに参照される、内部的なトークンプロバイダマップを管理しています。トークンはマッピングのためのキーとなっているのです。

これまでのすべてのサンプルでは、依存関係の値はクラスのインスタンスであり、クラスの型が独自の検索キーとして機能していました。ここでHeroService型をトークンとして与えることで、インジェクタからHeroServiceを直接取得します。

heroService: HeroService = this.injector.get(HeroService);

クラスベースの依存関係を注入するようコンストラクタに書いたとしても同様です。
HeroServiceクラス型でコンストラクタのパラメータを定義すると、AngularはそのHeroServiceクラストークンに関連するサービスを注入すればよいと察知します。

constructor(heroService: HeroService)

これは特に、ほとんどの依存関係の値がクラスによって提供されると考えるときに便利です。

非クラスの依存関係

依存関係の値がクラスでなかったとしたらどうでしょうか。場合によっては、挿入したいものが文字列や関数、またはオブジェクトであることもあります。

アプリケーションは、多くのちょっとしたこと(アプリケーションのタイトルや、Web APIのエンドポイントのアドレスなど)を設定用のオブジェクトによく定義していますが、これらの設定用オブジェクトは必ずしもクラスのインスタンスではありません。これらは次のようなオブジェクトリテラルです。

app/app-config.ts(excerpt)
export interface AppConfig {
  apiEndpoint: string;
  title: string;
}

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

この設定オブジェクトを注入できるようにしたいと思います。バリュープロバイダに、オブジェクトを登録できることはわかっています。

しかし、トークンとしては何を使うべきでしょうか。トークンとして機能するためのクラスがありません。AppConfigなんてクラスはないのです。

TypeScriptのinterfacesはトークン変数ではありません

HERO_DI_CONFIG定数にはインタフェースAppConfigがあります。 残念ながら、TypeScriptインターフェイスをトークンとして使用することはできません。

// FAIL!  Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

インタフェースが優先的に依存関係参照キーとなっている、厳密に型指定された言語のDIに慣れていると不思議な感じがします。

Angularのせいではありません。 インターフェイスは、TypeScriptのデザインタイムアーティファクトです。 JavaScriptにはインタフェースがありません。 TypeScriptのインターフェイスは、生成されたJavaScriptから消えてしまいます。 なので、実行時にAngularが見つけるべきインタフェースの型情報がないのです。

オペークトークン

非クラスにおける依存関係のプロバイダトークンを選択する場合、解決策の1つとしてオペークトークンを定義し、使用する方法があります。 定義は次のようになります。

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

export let APP_CONFIG = new OpaqueToken('app.config');

OpaqueTokenオブジェクトを使用して依存関係プロバイダを登録します

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

@Injectデコレータの助けを借りて、必要なコンストラクタに設定オブジェクトを注入することができます。

constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

AppConfigインターフェイスはDIでは何の役割も果たしませんが、クラス内で設定オブジェクトの型指定をサポートしています。

もしくは、`AppModuleのようなngModuleに対して設定オブジェクトを注入し、提供することができます。

app/app.module.ts_(ngmodule-providers)
providers: [
  UserService,
  { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],

オプション的な依存関係

HeroServiceLoggerを必要としますが、もしLoggerがなくても取得できるとしたらどうでしょうか。コンストラクタの引数に @Optional()を付けることによって、依存性がオプションであることをAngularに伝えることができます。

import { Optional } from '@angular/core';
constructor(@Optional() private logger: Logger) {
  if (this.logger) {
    this.logger.log(some_message);
  }
}

@Optional()を使う場合は、null用のコードを用意しないといけません。loggerをどこにも登録しないような場合、インジェクタはloggerの値をnullに設定します。

総論

この章では、AngularのDIの基本を学びました。様々な種類のプロバイダを登録することができ、コンストラクタにパラメータを追加して、注入されたオブジェクト(サービスなど)を要求する方法がわかりました。

AngularのDIは、ここで述べたことよりもさらに多様なことができます。 階層型DI の章では、ネストされたインジェクタのサポートから始めて、その高度な機能についてもっと詳しく学ぶことができます。

付録:インジェクタを直接操作する

インジェクタを直接動作させることはめったにありませんが、次のInjectorComponentでそれをやってみました。

app/injector.component.ts
@Component({
  selector: 'my-injectors',
  template: `
  <h2>Other Injections</h2>
  <div id="car">{{car.drive()}}</div>
  <div id="hero">{{hero.name}}</div>
  <div id="rodent">{{rodent}}</div>
  `,
  providers: [Car, Engine, Tires, heroServiceProvider, Logger]
})
export class InjectorComponent {
  car: Car = this.injector.get(Car);
  heroService: HeroService = this.injector.get(HeroService);
  hero: Hero = this.heroService.getHeroes()[0];
  constructor(private injector: Injector) { }
  get rodent() {
    let rousDontExist = `R.O.U.S.'s? I don't think they exist!`;
    return this.injector.get(ROUS, rousDontExist);
  }
}

Injector自体は注入可能なサービスです。

この例では、Angularはコンポーネント自身の Injectorを、コンポーネントのコンストラクタに注入しています。 コンポーネントは注入されたインジェクタに必要なサービスを問い合わせます。

サービス自体はコンポーネントに注入されないことに注意してください。それらはinjector.getを呼び出すことによって取り出されます。

要求されたサービスを解決できない場合、getメソッドはエラーを投げます。その代わり、このインジェクタ、またはより上位のインジェクタに登録されていないサービス(ROUS)を検索するために、第2のパラメータ(サービスが見つからない場合に返す値)でgetを呼び出すことができます 。

今回説明した手法は、サービスロケータパターンの例です。

本当に必要なときでなければ、この手法は避けてください。この手法だと、私たちがここで見てきたような、不注意なグラブバッグアプローチ(種々雑多な手法)を推進してしまうことになります。 これを説明し、理解し、テストすることは難しいです。コンストラクタを調べても、このクラスが何を必要とするのか、何を行うのかを知ることはできません。 この手法の場合、それ自身のサービスだけでなく任意の上位コンポーネントからサービスを取得することができるのですが、その実装が何であるかを見つけ出さなければならなくなります。

フレームワーク開発者がサービスを普遍的かつ動的に取得する必要がある場合に、このアプローチをとることがあります。

付録:ファイルごとに1つのクラスを推奨する理由

同じファイル内に複数のクラスを持たせると混乱するので、避けてください。 開発者はファイルごとに1つのクラスを望んでいます。 彼らを幸せにしてやってください。

このアドバイスを無視して……そう、同じファイル内で HeroServiceクラスとHeroesComponentクラスを組み合わせたりなんかしたときは、必ず最後にコンポーネントを定義してください!
サービスの前にコンポーネントを定義すると、アプリ起動時にnull値の参照エラーが返されます 。

このブログの記事で説明されているように、実際にはforwardRef()の助けを借りて最初にコンポーネントを定義することができます。しかし、なんで最初にそんな面倒なことをしないといけないのでしょうか?この問題はコンポーネントとサービスを別のファイルに定義することで回避してください。

Next Step
STYLE GUIDE - スタイルガイド -

10
4
0

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
10
4