本資料はAngularDartの公式ガイドのなかのDependency Injectionのページの日本語訳です。不明な個所は原文を参照願います。
本資料を読む前に「プログラミング言語Dartの基礎」等でDart言語の基礎知識を理解しておくことをお勧めします。
またIDEとしてIntelliJを使う場合は「AngularDartの開発環境をつくる」の章、または「プログラミング言語Dartの基礎」の16.2節のなかの「AngularDartのパッケージ」を参照願います。
このアプリのコードはGitHubからダウンロードできます。またライブで試すこともできます。更に筆者による「プログラミング言語Dartの基礎」の添付資料のdart_code_samplesのappsにも同じものがangular2_dependency-injectionとして含めてありますので、すでにdart_code_samplesが自分のIDE上にある場合はそちらも使えます。
最初に「クイック・スタート」の章の「IntelliJでこのアプリを開いて見てみよう」の節に従って、このアプリケーションをIDE上で開いてみましょう。IDE上でlibフォルダに入っているファイルたちを見ながら以下の説明を読むと理解が早いと思います。
********
依存物注入は重要なアプリケーション設計パタンです。Angularは自分の依存物注入フレームワークを持っており、我々は実際それなしではAngularのアプリケーションを構築できません。これは広く使われているので殆ど誰もがDIと呼んでいるほどです。この章ではDIとは一体何かそしてどうして必要なのかを学習します。次に我々はAngularアプリの中でこれをどう使うかを学習します。
ライブのサンプル(ソース・コードを見る)を走らせてみましょう。
以下のコードから始めましょう。
|
|
我々のCarはそのコンストラクタの中で必要なすべてを生成してしまっています?何が問題なのでしょうか?問題は我々のCarはもろくて柔軟性がなくまたテストが困難です。
我々のCarは engineとtiresが必要です。それらを頼むのではなく、CarのコンストラクタはEngineとTiresというそれに特化したクラスたちからのコピーをインスタンス化しています。
でもEngineクラスが進化してそのコンストラクタがパラメタを必要になってしまったらどうでしょうか?我々のCarは破たんし、engine = new Engine(theNewParameter)の行を書き換えない限り破たんしたままになってしまします。我々は最初にCarを書いたときはEngineコンストラクタのパラメタのことは気にかけませんでした。しかしEngineの定義が変わったとき、我々のCarクラスは変わらねばならないので、そのケアを始めねばなりません。それがCarをもろくしているのです。
我々のCarに別のブランドのタイヤ積みたいときはどうでしょうか?まず過ぎます。どのブランドがTiresクラスを生成するかに従属させられてしまいます。これがCarを柔軟性に欠けるものとしています。
現在新規の各carはそれ自身用のengineを持っています。あるengineを他のcarたちと共有することはできません。それは自動車のエンジンの場合は道理にかなっていますが、例えばそのメーカのサービス・センタへのオンボードの無線接続といった共有すべき他の依存物も考えることができます。我々のCarは他の消費者向けに作られたサービスを共有する柔軟性に欠けています。
我々のCar用のテストを書くときは我々はその隠れた依存物たちに翻弄されてしまします。テスト環境で新しいEngineを生成することさえもできますか?Engineそれ自身が何に依存していますか?その依存物は何に依存していますか?Engineの新規インスタンスがサーバに非同期呼び出しをしますか?我々はテスト中にそれが起きることは確実に望みません。
我々のCarがタイヤ圧力が低下したとき警告信号をフラッシュすべきですか?我々がテスト中に低圧力のタイヤに交換できないとしたら、それが実際に警告をフラッシュさせていることをどう確認しますか?
我々はそのcarの隠れた依存物たちに対してコントロールを持っていません。我々はその依存物たちをコントロールできないときは、classのテストは困難になります。
どうしたらCarをもっと堅牢で柔軟でまたテスト可能なものにできるでしょうか?
それは超簡単なのです。我々のCarのコンストラクタをDI付きのバージョンに変更しましょう:
|
|
これをDIなしと比較してみましょう:
|
|
なにが起きているのでしょうか?我々は依存物たちの定義をコンストラクタに移しました。我々のCarクラスはもはやengineあるいはtiresを生成しません。単にそれらを消費しているだけです。
我々はまたDartのコンストラクタ構文を活用し、パラメタたちの宣言とプロパティたちの初期化を同時に行っています。(訳者注:「プログラミング言語Dartの基礎」の「コンストラクタによるフィールドの初期化」の節を参照してください) |
これでengineとtiresをコンストラクタに渡すことでcarを生成します。
|
クールでしょう? engineとtiresの依存物たちの定義はCarクラス自身から切り離されています。それらがengineまたはtiresの一般的API要求を満足している限り、我々はどの種の好きなengineまたはtiresを渡すことができます。
もし誰かがEngineクラスを拡張したら、それはCarの問題ではありません。
Carの消費者(consumer)には問題が起きます。消費者はcar生成コードを例えば以下のように更新しなければなりません:
大事な点は this: Car自身は変える必要がなかったということです。この消費者の問題はあとで対処します。 |
我々がその依存物たちを完全に管轄しているので、このCarクラスのテストがはるかに簡単になります。我々は各テスト中に我々がまさしくやってほしいことをしてくれるモック(mocks:模擬物)たちをコンストラクタに渡すことができます。
|
これで依存物注入とは何かを学習しました。
これはあるクラスがその依存物たちを自分自身で生成するのではなくて外部の源から受け取るというコーディング・パタンです。
クールです! しかし消費者側が貧弱な場合はどうでしょうか?Carが欲しい誰もが3つのパーツであるCar、Engine、及びTiresの全部を生成しなければなりません。Carクラスはその問題たちを受け手側の犠牲のもとにまき散らします。これらのパーツの組立を面倒見てくれる何かが必要です。
それをやってくれる巨大なクラスを書くこともできましょう:
|
|
たった3つの生成メソッドだけの場合はそんなにまずくはありません。しかしアプリケーションが大きく伸びてゆくにつれこれの維持は難しくなっていきます。このファクトリは独立したファクトリのメソッドたちの蜘蛛の巣になろうとしています。
どの依存物をどこに注入するかを定義しなくとも良いように、作りたいと思う物たちのリストするだけで済むほうが良いと思いませんか?
ここが依存物注入のフレームワークが登場する場です。このフレームワークがインジェクタと呼ばれる何かを持っていると想像してください。我々はこのインジェクタを使って幾つかのクラスたちを登録し、そのインジェクタがそれらをどう生成するかを解いてくれます。
Carが必要になれば、我々は単にinjectorにたいしそれを持って来いと依頼すれば良く、それで準備ができたことになります。
|
誰もがウィン状態になります。CarはEngineとTiresの生成に関することは何も知りません。消費者側はCarの生成に関して何も知りません。我々は維持が必要な巨大なファクトリ・クラスを持ってはいません。Carと消費者の双方は単に必要なものを依頼するだけで、インジェクタが持ってきてくれます。
これが依存物注入のフレームワークがなにかの総てです。
依存物注入とはなにかが判り、その恵みに感謝するなかで、それをAngularでどのように実現するかを見てみましょう。
Angularは専用の依存物注入フレームワーク付きで出荷されています。またこのフレームワークは、他のアプリとフレームワークによってスタンドアロンのモジュールとして使うことも可能です。
面白そうです。Angularでコンポネントを構築する際にどうこれがやってくれるのでしょうか?一歩ずつやって行きましょう。
初めに The Tour of Heroesのなかで作ったHeroesComponentの簡素化バージョンから始めましょう。(訳者注:これらのファイルはGitHubからダウンロードした教材に入っています。)
|
|
|
|
(訳者注:Dartのコンストラクタのイニシャライザによるフィールドの初期化に慣れましょう。詳細は「プログラミング言語Dartの基礎」の「コンストラクタによるフィールドの初期化」の節を見てください。)
|
|
|
|
HeroesComponentはHeroesの特徴点表示領域のルート・コンポネントです。これはこの領域の総ての子供たちを統括しています。この必要最低限のバージョンでは子供はただ一つ HeroListComponentで、これはheroesのリストを表示します。
現在 HeroListComponentはHEROES(他のファイルmock_heroes.dartのなかで定義されたメモリ内のコレクションです)からheroesを取得しています。これは初期開発段階では十分でしょうが、とても理想的とは言えません。このコンポネントをテストしようとしたり、リモートのサーバからheroesのデータを取得したくなったときは、heroesのこの実装を変更し、このHEROESモック・データ以外の使用に対処しなければならなくなるでしょう。
heroデータをどう取得するかを隠したあるサービスを作ってみましょう。
このサービスは関心の分離(SoC:separate concern)を考えると、このサービス・コードはそれのためのファイルとして書くことをお勧めします。 |
|
|
このHeroServiseは以前と同じモック・データを返すgetHeroesメソッドを外部からアクセス可能にしていますが、その消費者たちの誰もがそれを知る必要はありません。
このサービス・クラスの上の@Injectable()アノテーションに注目願います。その目的は後で説明します。 |
我々はあえてこれが実際のサービスだとも言っていません。もしわれわれが実際にリモートのサーバからデータを得るときは、このAPIは非同期となり、Futureオブジェクトを返すことになりましょう。我々はまたコンポネントたちが自分たちのサービスを消費する方法も書き換えねばならなくなります。これは一般的に重要なことですが、今のストーリにはそうではありません。 |
Angularに於いてはサービスはクラス以外の何物でもありません。これをAngularのインジェクタに登録しない限りこれはクラス以外の何物でもない状態に留まります。
我々はAngularのインジェクタを生成する必要はありません。Angularはブートストラップの過程でアプリケーションにわたるインジェクタを生成します。
|
bootstrap(AppComponent);
|
我々は自分たちのアプリが必要としているサービスを生成するプロバイダたちを登録することでインジェクタを設定しなければなりません。我々は本章の後の箇所でプロバイダとは何かを説明します。
我々が設定する前に、ブートストラップ中のプロバイダ登録の例を見てみましょう:
|
インジェクタはこれでHeroServiceに関して理解します。我々のHeroServiceのインスタンスは我々のアプリにわたって注入のため利用可能になります。
無論コメントがこのようにするなと言っていることにいぶかるでしょう。これは動作します。しかしベスト・プラクティスではありません。ブートストラップのプロバイダのオプションはAngular自身に登録されているサービスたち(例えばルーティング・サポート)を設定し、オーバライドすることを意図したものです。
好ましいアプローチはアプリケーション・プロバイダたちをアプリケーション・コンポネントたちの中で登録することです。HeroService はHeroesの特徴点表示領域内で使われ、そして他では使われないので、それの理想的な登録場所はトップ・レベルのHeroesの中です。
以下はHeroServiceを登録するよう書き換えた HeroesComponentです。
|
import 'package:angular2/core.dart'; import 'hero_list_component.dart'; import 'hero_service_provider.dart'; @Component( selector: 'my-heroes', template: ''' <h2>Heroes</h2> <hero-list></hero-list>''', providers: const [heroServiceProvider], directives: const [HeroListComponent]) class HeroesComponent {} |
@Componentアノテーションの中のproviders:部分を見てください。 HeroServiceのインスタンスはこの HeroesComponent及びその総ての子供たちのなかで注入のため利用可能となります。
HeroesComponent自身はたまたまこの HeroServiceのなかでは必要とされていません。しかしその子供の HeroListComponentは必要としており、次にそれを示します。
HeroListComponentは注入された HeroServiceからのheroesを取得しなければなりません。依存物注入のパタンに従い、以前説明したように、このコンポネントはそのコンストラクタの中でこのサービスを要求しなければなりません。これは小さな変更です:
|
import 'package:angular2/core.dart'; import 'hero.dart'; import 'hero_service.dart'; @Component( selector: 'hero-list', template: ''' <div *ngFor="let hero of heroes"> {{hero.id}} - {{hero.name}} </div>''') class HeroListComponent { final List<Hero> heroes; HeroListComponent(HeroService heroService) : heroes = heroService.getHeroes(); } |
|
import 'package:angular2/core.dart'; import 'hero.dart'; import 'mock_heroes.dart'; @Component( selector: 'hero-list', template: ''' <div *ngFor="let hero of heroes"> {{hero.id}} - {{hero.name}} </div>''') class HeroListComponent { final List<Hero> heroes = HEROES; } |
コンストラクタに注目してください: ここでおきているのはコンストラクタにパラメタを付加することだけではありません。
コンストラクタのパラメタの型は HeroServiceで、HeroListComponentは@Componentアノテーションを持っていることに注意してください。また、親のコンポネント(HeroesComponent)はHeroServiceのためのプロバイダ情報を持っていることも思い出してください。 コンストラクタのパラメタ型、@Componentアノテーション、及び親のプロバイダ情報が一緒になってAngularのinjectorにたいし新規の HeroListComponentを生成したときは何時でも HeroServiceのインスタンスを注入するように知らせています。 |
上の節の中でインジェクタのアイデアを説明したとき、新規のCarを生成するときにこれをどう使うかを示しました。ここではまたそのようなインジェクタがどのように明示的に生成できるかを示します。
|
我々は Tour of Heroesあるいは他のどのサンプルでもこのようなコードはありません。もしそうしなければならない場合は(滅多にないことですが)明示的にインジェクタを生成するコードを書くことは可能です。Angularはコンポネントたちを生成するときは-<hero-list></hero-list>のようなHTMLマークアップを介して、あるいはルータであるコンポネントにナビゲートした後で-インジェクタの生成と呼び出しも面倒を見ます。我々がAngularにその仕事をさせる場合は、自動化された依存物注入の利益を享受することになります。
依存物たちはインジェクタのスコープ内においてシングルトン(唯一のインスタンスしか持たない)です。我々の例では、単一の HeroServiceのインスタンスが HeroesComponentとその子供たちによって共有されています。
しかしながらAngular DIは階層的な注入システムであり、これはネストしたインジェクタたちがそれぞれのサービス・インスタンスを生成できることを意味します。これは階層的インジェクタの章で学習願います。
以前依存物注入のクラスを設計すればそのクラスのテストがしやすくなると強調しました。依存物たちをコンストラクタのパラメタたちとしてリストすることは、我々すべてがアプリのパーツたちを効果的にテストするのに必要なものでしょう。
例えば、新しい HeroListComponentをモック・サービスでつくり、テスト化でいじれるようにできます:
|
テストに関してはこちらを読んでください。 |
我々のHeroServiceは非常にシンプルです。それ自身は何ら依存物を持っていません。
もしこれが依存物を持っていたらどうでしょうか?もしロッギング・サービスを介してその動作を報告するとしたらどうでしょうか?我々は同様のコンストラクタ注入パタンを適用し、Loggerパラメタをとるコンストラクタを付加することになります。
以下は改定版とオリジナルとの比較です。
|
import 'package:angular2/core.dart'; import '../logger_service.dart'; import 'hero.dart'; import 'mock_heroes.dart'; @Injectable() class HeroService { final Logger _logger; HeroService(this._logger); List<Hero> getHeroes() { _logger.log('Getting heroes ...'); return HEROES; } } |
|
import 'package:angular2/core.dart'; import 'hero.dart'; import 'mock_heroes.dart'; @Injectable() class HeroService { List<Hero> getHeroes() => HEROES; } |
今度はコンストラクタはLoggerの注入されたインスタンスを要求し、それをプライベートな_loggerと呼ばれるプロパティにストアします。我々は誰かがheroesを要求してきたときに我々のgetHeroesメソッドのなかでそのプロパティを呼びます。
@Injectable()はあるクラスがインスタンス化のためインジェクタが使えることをマークしています。一般的に言えば、インジェクタは@Injectable()とマークされていないクラスをインスタンス化しようとすればエラーを報告します。
インジェクタたちはまた HeroesComponentのようなコンポネントたちのインスタンス化の責も持ちます。それならどうして HeroesComponentを@Injectable()とマークしないのでしょうか?
もし実際にそうしたいと思うのならそれを付加することは可能です。しかしHeroesComponentは既に@Componentでマークされており、このアノテーション・クラスは(後で学習することになる@Directiveと@Pipe同様に)injectableの副型なので、これは不必要です。実際これはインジェクタたちによってインスタンス化のターゲットとしてのクラスだと特定するinjectableアノテーションなのです。
丸括弧()を忘れないこと |
単に@Injectableではなく必ず@Injectable()と書いてください。メタデータ・アノテーションはコンパイル時常数コンスタント変数への参照、またはInjectable()のような常数コンストラクタ呼び出しのどちらかでなければなりません。 もし丸括弧丸かっこを忘れると、アナライザが文句を言います:"Annotation creation must have arguments"。もしこのアプリをとにかく走らせ、動作しないときは、コンソールは次のように表示します:"expression must be a compile-time constant" |
我々は自分たちのHeroServiceにロガーを2段階で注入しました:
ロガー・サービスの作成
それをこのアプリで登録
我々のロガー・サービスは極めてシンプルです:
|
import 'package:angular2/core.dart'; @Injectable() class Logger { List<String> _logs = []; List<String> get logs => _logs; void log(String message) { _logs.add(message); print(message); } } |
実際の実装では多分logging packageを使うことになります。 |
我々は多分自分のアプリの至る所で同じロガー・サービスが必要になるので、これはこのプロジェクトのlibフォルダの中に置き、我々のアプリのコンポネントであるAppComponentの providersリストのなかにこれを登録しています。
|
providers: const [ Logger ]) |
もしロガーの登録を忘れると、Angularは最初にloggerを探すときに例外をスローします:
|
これがAngularが我々に依存物のインジェクタがloggerのプロバイダを見つけられなかったことを告げています。そのプロバイダが新規のHeroServiceのなかに注入するLoggerを生成するためのプロバイダが必要だったのです。そしてそれが生成し新規の HeroListComponentに注入するのに必要だったものだったのです。
この生成チェインはLoggerのプロバイダから始まっています。プロバイダたちは次節での対象です。
プロバイダは依存物の値の具体的なランタイム・バージョンを提供します。インジェクタは該インジェクタがコンポネントたちや他のサービスたちに注入するサービスのインスタンスを生成するのにプロバイダたちに頼ります。
我々はサービス・プロバイダをinjectorに登録しなければなりません。そうしないとインジェクタはどうそのサービスを生成するかわからなくなります。
前に我々は以下のようにAppModuleのメタデータのproviders:リストにLoggerサービスを登録しました:
providers: const [Logger
|
Loggerを実装した何かを提供するにはいろんなやり方があります。Loggerクラス自身明らか且つその役割からしてプロバイダであります。しかしこれが唯一のものではありません。
我々はLoggerを提供できる代替的プロバイダでインジェクタを設定できます。我々は代替クラスを提供することもできます。我々はそれにロガー・ファクトリ関数を呼ぶプロバイダを与えることもできます。これらのアプローチのどれもが正しい状況の下では良い選択肢となり得ます。
問題はインジェクタがLoggerを必要になったときしかるべきプロバイダが存在するということです。
我々は少し前に次のようにprovidersのところを書きました:
providers: const [Logger
|
これは実際はProviderクラスの新規インスタンスを生成するプロバイダ登録の省略型の式なのです:
|
我々は2つ(あるいはそれ以上)の引数をProviderコンストラクタに渡しています。
最初は依存物の値を見つけるため、及びプロバイダを登録するため、の双方のキーとしてのトークンです。
2番目は useClassといった指名パラメタで、これは依存物の値を生成するためのレシピとして考えることができるものです。依存物の値たちを生成するには多くの方法があります....そしてレシピを書く方法も沢山あります。
時にはこのサービスを提供するのに別のクラスを要求することもあるでしょう。以下のコードはインジェクタに対し何かがLoggerを求めてきたとき BetterLoggerを返すよう伝えています。
|
EvenBetterLoggerがログ・メッセージにユーザ名を表示できるとしましょう。このロガーはUserServiceから注入されたuserを取得し、そのUserServiceもまたアプリのレベルで注入されるものだとしましょう。
|
これをBetterLoggerでやったように設定します。
|
古いコンポネントがOldLoggerクラスに依存しているとしましょう。OldLoggerはNewLoggerと同じインターフェイスを持っているが、何らかの理由でそれを使うのに古いコンポネントのほうをアップデートできないとしましょう。
古いコンポネントがOldLoggerを使ってあるメッセージをログする際、それを代わりに処理するのにNewLoggerのシングルトンのインスタンスを使いたいとします。
コンポネントが新らしいまたは古いloggerのいずれかを要求したときは依存物のインジェクタはそのシングルトンのインスタンスを注入しなければなりません。OldLoggerは NewLoggerのエイリアスでなければなりません。
我々は確かに自分たちのアプリの中で2つの異なったNewLoggerのインスタンスがあることは望みません。残念ながら、useClassでNewLoggerの別名をOldLoggerにしようとしてもそうなってしまします。
|
解決法はuseExistingのオプションを使って別名化することです:
|
時にはインジェクタにたいしあるクラスからそれを生成するよう依頼するよりも、出来合いのオブジェクトを用意したほうがもっと簡単です。
Dartの相違点:メタデータ内の常数 |
Dartでは、メタデータ・アノテーションの値はコンパイル時常数でなければならないので、しばしばuseValueが文字列またはリスト・リテラルを使って利用されます。しかしながらuseValueはどの常数のオブジェクトに対しても機能します。 常数オブジェクトが提供できるクラスを作るには、そのインスタンス変数の総てがfinalであるようにし、それにconstコンストラクタを与えねばなりません。 newではなくてconstを使うことでそのクラスの常数オブジェクトを作ります。 |
|
次にプロバイダにuseValueオプションで登録します。このオプションでこのオブジェクトにロガーの役割を持たせます。
|
「非クラスの依存物」及び「OpaqueToken」の節のなかに更にuseValueの使用例があります。
最後の起き得る瞬間まででしか得られない情報に基づいて、ダイナミックに依存物の値を生成する必要がある場合があります。多分その情報はブラウザのセッションの過程の中で繰り返し的に変化します。
またインジェクト可能なサービスがこの情報のソースへの独立したアクセスを持っていないとしましょう。
この状況がファクトリ・プロバイダ(factory provider)の出番になります。
新しいビジネス要求…HeroServiceは通常のユーザからは を隠さねばならない..オーソライズされたユーザのみがsecret heroesを見れる…を加えることで図示しましょう。
EvenBetterLoggerのように、HeroServiceはユーザに関するファクトが必要です。このサービスはそのユーザが secret heroesを見る権限を持っているかを知る必要があります。この権限は単一のアプリケーションのセッションの最中に変化することがあり得ます。例えば別のユーザがログインしたときがそうです。
EvenBetterLoggerと違うのは、HeroServiceにUserServiceを注入できないことです。 HeroServiceは誰が権限を持っているかあるいは持っていないかを判断する為のユーザ情報への直接アクセスを持っていません。
どうして?我々にもわかりません。この手のことは起きるのです。 |
代わりにHeroServiceのコンストラクタが秘密のheroesの表示を制御するためのブール値のフラグをとります。
|
final Logger _logger; final bool _isAuthorized; HeroService(this._logger, this._isAuthorized); List<Hero> getHeroes() { var auth = _isAuthorized ? 'authorized' : 'unauthorized'; _logger.log('Getting heroes for $auth user.'); return HEROES .where((hero) => _isAuthorized || !hero.isSecret) .toList(); } |
我々はLoggerを注入できますが、ブール値のisAuthorizedは注入できません。我々はこのHeroServiceの新規インスタンスの生成を乗っ取って代わりにファクトリのプロバイダを使わねばなりません。
ファクトリのプロバイダはファクトリの関数が必要です:
|
HeroService heroServiceFactory(Logger logger, UserService userService) => new HeroService(logger); |
HeroServiceはUserServiceへのアクセスを持っていませんが、このファクトリ関数は持っています。
我々はLoggerと UserServiceの双方をこのファクトリのプロバイダに注入し、インジェクタにたいしそれらをこのファクトリ関数と一緒に渡させるようにします:
|
const heroServiceProvider = const Provider(HeroService, useFactory: heroServiceFactory, deps: const [Logger, UserService]); |
useFactoryのフィールドはAngularに対しこのプロバイダはファクトリ関数であって、その実装はheroServiceFactoryであることを知らせています。 depsプロパティはプロバイダ・トークンたちのリストです。 LoggerとUserServiceのクラスはそれ自身のクラスのプロバイダのためのトークンとして機能します。インジェクタはこれらのトークンたちを解決し、対応したサービスたちをマッチしたファクトリ関数のパラメタたちに注入します。 |
我々はこのファクトリのプロバイダを常数のheroServiceProviderの中で捕捉したことに注意願います。この追加のステップでこのファクトリのプロバイダを再利用可能にしています。我々は必要な時は何時でもこの常数で我々の HeroServiceを登録できます。
我々のサンプルでは、HeroesComponentの中でのみこれが必要でした。 HeroesComponentのなかではこれはこれまでのメタデータのプロバイダ・リストのHeroService登録を置き換えています。以下のコードたちで新しい実装と古い実装を一行ずつ比較してみてください:
|
import 'package:angular2/core.dart'; import 'hero_list_component.dart'; import 'hero_service_provider.dart'; @Component( selector: 'my-heroes', template: ''' <h2>Heroes</h2> <hero-list></hero-list>''', providers: const [heroServiceProvider], directives: const [HeroListComponent]) class HeroesComponent {} |
|
import 'package:angular2/core.dart'; import 'hero_list_component.dart'; import 'hero_service.dart'; @Component( selector: 'my-heroes', template: ''' <h2>Heroes</h2> <hero-list></hero-list>''', providers: const [HeroService], directives: const [HeroListComponent]) class HeroesComponent {} |
インジェクタに対しあるプロバイダを登録する際、我々はそのプロバイダを依存物注入トークンと関連付けます。インジェクタは依存物が要求された際それが参照しているトークン/プロバイダの内部マップを維持しています。このトークンはそのマップのキーです。
これまでのサンプルの総てで、依存値はあるクラスのインスタンスでした。そしてそのクラスの型はそれ自身の検索キーとして機能していました。ここではHeroServiceの型をトークンとして供給することでインジェクタから直接HeroServiceを取得しました:
heroService = _injector.get(HeroService);
|
我々がインジェクタ・クラス・ベースの依存物を要求するコンストラクタを書くときは、似たような良い機能が使えます。我々はHeroServiceクラスの型でコンストラクタのパラメタを定義し、それでAngularはそのHeroServiceクラスのトークンに関連付けられたサービスを注入することを知ります:
HeroListComponent(HeroService heroService)
|
ほとんどの依存物の値たちはクラスたちによって提供されていることを考えれば、これは特に便利なものです。
依存物の値がクラス以外のものだったらどうでしょうか?時には我々が注入したいものはString、List、Mapのオブジェクトあるいは関数(これもオブジェクトですが)であることがあります。
アプリケーションはしばしば多くの小さなファクトたち(そのアプリのタイトルあるいはウェブAPIのエンドポイントのアドレスといった)をもった設定オブジェクトたちを定義しています。それらは以下のようなMapリテラルであり得ます:
|
const Map heroDiConfig = const <String, String>{ 'apiEndpoint': 'api.heroes.com', 'title': 'Dependency Injection' }; |
我々はこの設定オブジェクトが注入のため使えるようにしたいとします。我々は値のプロバイダにあるオブジェクトを登録できることを知っております。
しかしトークンとして何を使うことになるのでしょうか?Mapは使えますが、Mapは(Stringのように)あまりにも一般的なものです。我々のアプリは幾つかのmapたちに依存しその各々が異なった用途用であるかもしれません。
Dartの相違点:インターフェイスは有効なトークンです |
TypeScriptではインターフェイスはプロバイダのトークンとしては機能しません。Dartにはそのような制限はありません;各クラスは暗示的にインターフェイス定義しており、従ってインターフェイス名は単にクラス名なのです。Mapはその名前が抽象クラスの名前であっても有効なトークンです;それは余りにも一般的なものなのでトークンとしては不適切なだけです。 |
非クラスの依存物たちのプロバイダ・トークンを選択する際の一つのソリューションはOpaqueTokenを定義し使うことです。定義は以下のようなものになります:
import 'package:angular2/core.dart'; const APP_CONFIG = const OpaqueToken('app.config'); |
我々はこのOpaqueTokenオブジェクトを使って依存物のプロバイダを定義します:
providers: const [ const Provider(APP_CONFIG, useValue: heroDiConfig)] |
これで我々はこの設定オブジェクトを、それが必要としているどのコンストラクタへも、@Injectアノテーションの助けを借りて注入できます。
AppComponent(@Inject(APP_CONFIG) Map config) : title = config['title']; |
Mapインターフェイスは依存物注入では何の役割もしていませんが、このクラス内の設定オブジェクトのタイピングには(config['title'];のように)寄与しています。 |
設定Map使用の代替として、我々はカスタムの設定クラスを定義することができます:
|
class AppConfig { String apiEndpoint; String title; } AppConfig heroDiConfigFactory() => new AppConfig() ..apiEndpoint = 'api.heroes.com' ..title = 'Dependency Injection'; |
設定クラスを定義することは少しばかりメリットがあります。一番のメリットは強い静的チェッキングです:プロパティ名をミススペルしたとき、あるいは間違った型の値を代入しようとしたときに初期段階で警告を受けます。Dartのカスケード記述(..)は設定オブジェクトの初期化には便利な手段です。
もしわれわれがカスケードをつかうなら、この設定オブジェクトはconstと宣言できないし、値のプロバイダを使うことはできません。その解決法はファクトリ・プロバイダを使うことです。我々は以下にそれを示します。我々はまた我々のトップ・レベルのAppComponentのなかにこの設定オブジェクトをどう注入するかも示します:
|
providers: const [ Logger, UserService, const Provider(APP_CONFIG, useFactory: heroDiConfigFactory) ]) |
|
AppComponent(@Inject(APP_CONFIG) AppConfig config, this._userService) : title = config.title; |
我々のHeroServiceはLoggerを必要としていますが、ロガーなしで何とかやってゆけるとしたらどうなるでしょう?我々はコンストラクタの引数を@Optional()でアノテートすることで、Angularに対しこの依存物はオプショナルだと知らせることができます:
HeroService(@Optional() this._logger) { _logger?.log(someMessage); } |
@Optional()を使うと、我々のコードはnull値の備えをしなければなりません。もしこの行の前のどこかでloggerを登録していないと、インジェクタはloggerの値としてnullをセットします。
我々は本章でAngularの依存物注入の基本事項を学習しました。我々は多種のプロバイダたちを登録できるし、我々はコンストラクタにあるパラメタを付加することで注入されたオブジェクト(サービスのような)をどのように要求するかを知りました。
Angularの依存物注入は我々が説明してきた以上の能力を有します。我々はさらに進んだ機能たちを、ネストされたインジェクタたち対応から始めて、「階層化された依存物注入」の章で学習できます。
我々はインジェクタを直接操作することは滅多にありませんが、それをする 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: const [Car, Engine, Tires, heroServiceProvider, Logger]) class InjectorComponent { final Injector _injector; Car car; HeroService heroService; Hero hero; InjectorComponent(this._injector) { car = _injector.get(Car); heroService = _injector.get(HeroService); hero = heroService.getHeroes()[0]; } String get rodent => _injector.get(ROUS, "R.O.U.S.'s? I don't think they exist!"); } |
Injector自身は注入可能なサービスです。
この例では、Angularはこのコンポネントの自らのInjectorをこのコンポネントのコンストラクタに注入しています。このコンポネントは次に注入されたインジェクタに対し欲しいサービスを依頼しています。
このコンポネントにはサービスたち自身は注入されていないことに注意してください。それらは injector.getを呼ぶことで取り出せます。
このgetメソッドは要求されたサービスが解決できないときはエラーをスローします。我々は代わりに2番目のパラメタ(もし該サービスが見つからないときに返す値)つきでgetを呼べます。それがこれあるいはその先祖のインジェクタで登録されていないサービス(ROUS)を検索ひとつの例でやったことです。
ここで示した技術はサービス・ロケータ・パタンの一例です。 純粋にそれが必要にならない限りこの技術を我々は避けています。これはここで見たような不注意な宝探し袋のアプローチを奨励させてしまいます。この技術は説明、理解、そしてテストが困難です。このクラスが何を要求しているか或いは何をするかをこのコンストラクタを調べるだけでは解り得ません。これが何をしているかを発見するのにこの実装を探検する羽目に陥ります。 フレームワークのデベロッパたちは一般的かつ動的にサービスを取得しなければならないときにこのアプローチをとるかもしれません。 |