トピック
Angular2 CORE DOCUMENTATIONのGUIDEの翻訳です。
- DOCUMENTATION OVERVIEW - ドキュメント概要 -
- ARCHITECTURE OVERVIEW - 構文概要 -
- DISPLAYING DATA - データ表示 -
- USER INPUT - ユーザー入力 -
- FORMS - フォーム -
- DEPENDENCY INJECTION - 依存性注入 -
- STYLE GUIDE - スタイルガイド -
注意1)ここに掲載されていない項目は、Angular2 CORE DOCUMENTATIONのGUIDEを直接参照してください。
注意2)2016年11月22日時点の翻訳です。翻訳者はTOEICで700点くらいの英語力なので、英訳が間違っている可能性があります。しかもかなり意訳している箇所もあります。もし意訳を通り越して、誤訳になっているような箇所がありましたらご指摘ください。
DEPENDENCY INJECTION - 依存性注入 -
Angularの依存性注入(DI)は、"必要なときに、必要なだけ"依存するサービスを作り出し、運用するシステムです。
依存性注入は、重要なアプリケーションデザインパターンです。Angularには独自の依存性注入フレームワークがあり、Angularのアプリケーションはそれなしに構築できません。そしてそれは多くの人にDIと呼ばれ、広く使われています。
この章では、DIとはどんなものか、なぜDIを使うのかといったことを学習します。そのあと、実際にAngularアプリの中での使用方法について学んでいきます。
live exampleを起動してみてください。
なぜ依存性注入なのか
では早速、次のコードを使って始めてみましょう。
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
のコンストラクタが特殊なクラスであるEngine
とTires
からそのコピー(インスタンス)を作成しています。
もしEngine
クラスが更新され、そのコンストラクタがパラメータを要求してきたらどうなるでしょうか。
Car
は壊れてしまい、this.engine = new Engine(theNewParameter)
と行を書き直すまで壊れたままになります。もし初めてCar
を書いたというのなら、Engine
にかかるコンストラクタのパラメータがなんであっても構いません。なので、今のところは何も気にする必要はないのですが、Engine
の定義が変更され、Car
を変更しなければならなくったときのために、あらかじめ気にしておかなければならないのです。それがCar
の脆弱性へとつながります。
では、もし違うブランドのタイヤをCar
に取り付けたくなったとしたらどうでしょうか。最悪です。Tires
クラスが作ったブランドの場合、どんなものであってもそのブランドしか選べなくなります。それがCar
の柔軟性のなさへとつながります。
今すぐ新しい車に、独自のエンジンを取り付けてみましょう。エンジンをほかの車と共有することはできません。車のエンジンはそれで問題ありませんが、メーカーのサービスセンターへつなぐ車内無線のような、共有されるべきほかの依存性についてはどうでしょうか。Car
は、あらかじめほかの消費者のために作られたサービスを共有するといった柔軟性を持ち合わせていません。
Car
のテストを書こうとすると、その隠れた依存性に翻弄されることになります。テスト環境で、新しいEngine
を作ることは可能でしょうか。Engine
そのものが依存しているものは何でしょうか。その依存しているものは、何に依存しているのでしょうか。Engine
の新しいインスタンスは、サーバーへ非同期通信を行うのでしょうか。テスト中、ずっとそんなことをやっているなんてどんでもないです。
タイヤの空気圧が少なくなったとき、Car
は警告信号を発すべきでしょうか。テスト中に空気圧が少ないタイヤに交換できないとなったら、実際に発せられる信号をどうやって確認すればよいのでしょうか。
その車の隠れた依存性をコントロールする術はありません。依存性の管理ができないとなると、クラスのテストは難しくなります。
では、どうすればCar
をより強固で、柔軟性のある、テストしやすいものにできるのでしょうか?
その答えは超簡単です。Car
のコンストラクタを、DIを使ったバージョンに変えればいいのです。
public description = 'DI';
constructor(public engine: Engine, public tires: Tires) { }
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
をほしい人はみんな、Car
、Engine
、Tires
という3つの部品すべてを作らないといけなくなりました。Car
クラスは購買者が負担するという問題を抱えています。なので、これらの部品の組み立てに配慮してくれる何かが必要となります。
組み立てをやってくれる巨大なクラスを書いてみました。
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);
これですべてが上手くいきました。Car
はEngine
やTires
を作ることについて何も知りませんし、購買者はCar
の作り方について何もわかっていません。メンテナンスが必要な巨大なファクトリークラスなんて必要ないのです。Car
と購買者はとにかく必要なものだけを要求しておけば、あとはインジェクタがうまくやってくれます。
DIフレームワークとはこういうものです。
さて、DIとは何なのかがわかりましたので、その恩恵に感謝しつつ、Angularでの使いかたを見ていきましょう。
Angularの依存性注入
Angularは独自のDIフレームワーク持っています。このフレームワークは、異なるアプリケーションやフレームワークで、独立したモジュールとして使用することもできます。
よさそうな感じですが、Angularのコンポーネントを構築するときには何をすればよいのでしょうか。
1つずつ見ていきましょう。
The Tour of Heroesで構築したHeroesComponent
の簡易版から始めてみます。
import { Component } from '@angular/core';
@Component({
selector: 'my-heroes',
template: `
<h2>Heroes</h2>
<hero-list></hero-list>
`
})
export class HeroesComponent { }
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;
}
export class Hero {
id: number;
name: string;
isSecret = false;
}
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' }
];
HeroesComponent
はHeroesの機能領域におけるルートコンポーネントです。この領域にあるすべての子コンポーネントを制御しています。この縮小版では、HeroListComponent
というヒーローのリストを表示する子を1つだけにしました。
早速HeroListComponent
で、ほかのファイルで定義されているHEROES
というメモリ上のコレクションからヒーローを取得してみます。開発の早い段階ではそれで十分かもしれませんが、理想はだいぶ遠いところにあります。できるだけ早くこのコンポーネントでテストを試し、リモートサーバからヒーローのデータを取得したいところです。それが終わったら、heroes
の実装を変更し、HEROES
モックデータが使われているすべての箇所を修正する必要があります。
では、ヒーローデータを取得しているところを隠すサービスを作ってみましょう。
サービスはseparate concern(独立した関係)であるべきなので、サービスのコードは独自のファイルに書くことをお勧めします。
この記述で詳細を確認してください。
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が起動する過程の中で、アプリケーションを範囲としたインジェクタが作成されます。
platformBrowserDynamic().bootstrapModule(AppModule);
アプリケーションが要求するサービスを作るprovidersを登録することで、インジェクタを設定する必要があります。providersがどんなものなのかは、この章の後半で説明します。
また、プロバイダはNgModuleやアプリケーションコンポーネントにも登録することができます。
NgModuleにprovidersを登録する
ここでAppModuleにLogger
、UserService
、APP_CONFIG
プロバイダを登録します。
@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を登録する
ここではHeroService
をHeroesComponent
に登録するよう修正を加えます。
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
サービスはアプリケーションのどこからでもアクセスできるようにしておきたいですが、HeroService
はHeroes機能の範囲に留め、それ以外からは使われないようにしておきます。
NgModule FAQの章にあるアプリを範囲としたprovidersをルートの
AppModule
に加えるべきか、AppComponent
に加えるべきかも読んでおいてください。
HeroListComponentのインジェクションを用意する
HeroListComponent
は注入されたHeroService
からheroesを取得すべきです。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();
}
}
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
)はHeroService
のproviders
情報を持っていることも思い出してください。新しい
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
パラメータを持ったコンストラクタを加え、同じようにコンストラクタインジェクションパターンを適用してみましょう。
オリジナルと比較できる修正版を用意しました。
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;
}
}
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を注入してみます。
- loggerサービスを作成します。
- アプリケーションにそれを登録します。
loggerサービスは極めてシンプルです。
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
配列として登録しておきます。
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
クラスに依存しているとします。 OldLogger
はNewLogger
と同じインターフェースを持っていますが、何らかの理由でそれを使用するために古いコンポーネントを更新することはできません。
古いコンポーネントが OldLogger
でメッセージを記録するとき、OldLogger
ではなくNewLogger
のシングルトンインスタンスで処理することが望ましいです。
依存関係のあるインジェクタは、コンポーネントが新しいロガーまたは古いロガーのどちらかを要求するときに、そのシングルトン・インスタンスを挿入する必要があります。 OldLogger
はNewLogger
のエイリアスでなければなりません。
このアプリでは2つの異なる NewLogger
インスタンスを必要としないのですが、残念なことにuseClass
で OldLogger
をnewLogger
に エイリアスしようとすると、それが得られてしまいます。
[ 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
と違うのは、HeroService
に UserService
を注入することができないということです。HeroService
は許可された人とされていない人を判断しようとして、ユーザ情報へ直接アクセスするということができません。
なんででしょうか。わかりません。こうなります。
その代わり、HeroService
コンストラクタは、秘密のヒーローの表示を制御するブール値のフラグを持ちます。
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
のインスタンスを作り、引き継いでおく必要があります。
ファクトリープロバイダには、ファクトリーファンクションが必要です。
let heroServiceFactory = (logger: Logger, userService: UserService) => {
return new HeroService(logger, userService.user.isAuthorized);
};
HeroService
だとUserService
にアクセスする方法がありませんが、ファクトリーファンクションならアクセス可能です。
Logger
とUserService
の両方をファクトリープロバイダに注入し、ファクトリーファンクションを使ってそれらをインジェクタに渡します。
export let heroServiceProvider =
{ provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService]
};
useFactory
フィールドは、プロバイダがheroServiceFactory
を実装したファクトリーファンクションであることをAngularに伝えます。
deps
プロパティはプロバイダトークンの配列です。Logger
やUserService
のクラスは独自のプロバイダクラスプロバイダを持つトークンとして保存されます。インジェクタはこれらのトークンを解決し、ファクトリーファンクションのパラメータと照合して一致するサービスに注入を行います。
ファクトリープロバイダをエクスポートされた変数heroServiceProvider
で取得したことに注目してください。 この追加の手順によって、ファクトリープロバイダが再利用可能になります。 必要があれば、この変数を使ってどこからでもHeroService
を登録することができます。
サンプルを見返してみると、メタデータproviders
の配列にあるHeroService
を置き換える必要があるのはHeroesComponent
だけです。次の例では、新しい実装と古い実装を並べて表示しています。
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 { }
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のエンドポイントのアドレスなど)を設定用のオブジェクトによく定義していますが、これらの設定用オブジェクトは必ずしもクラスのインスタンスではありません。これらは次のようなオブジェクトリテラルです。
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に対して設定オブジェクトを注入し、提供することができます。
providers: [
UserService,
{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],
オプション的な依存関係
HeroService
はLogger
を必要としますが、もし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
でそれをやってみました。
@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 - スタイルガイド -