Skip to content

Instantly share code, notes, and snippets.

@dolpen
Last active August 10, 2016 02:37
Show Gist options
  • Save dolpen/83f77732beea2226247db7da496cd9a7 to your computer and use it in GitHub Desktop.
Save dolpen/83f77732beea2226247db7da496cd9a7 to your computer and use it in GitHub Desktop.

サービス

再利用可能な**サービス(Services)**を作り、ヒーローのデータ呼び出しを管理する

サービスとは

アプリケーションは進化を続けている。近いうち、たくさんのコンポーネントを追加することになるだろう。 それらのコンポーネントはヒーローのデータへアクセスする必要があるだろう。 しかし、その度にヒーローのデータをコピー&ペーストはしたくない。 その代わりに、今回は単一、かつ再利用できるサービスを作り、データが必要なコンポーネントに注入する方法を学ぶ。

データへのアクセスを分離されたサービスに再構築する(置き直す)ことは、 コンポーネントの機能を表示という目的に絞り込み注力させるという状態を維持することになる。 また、サービス自体をモック(ダミー)に置き換えることでユニットテストもしやすくなる。

サービスは常に非同期処理であるため、この章の最終的な目的は Promise パターンを利用したサービスを作ることとする。

今回作るもののデモ

前回までのあらすじ

この章を続ける前に、まず前回の章が終わった時のフォルダ構造を確認してみよう。 もし以下のようになっていないなら、前の章に戻ってどこが足りないのかを探してみてほしい。

angular2-tour-of-heroes
├app
│├app.component.ts
│├hero.ts
│├hero-detail.component.ts
│└main.ts
├node_modules ...
├typings ...
├index.html
├package.json
├styles.css
├tsconfig.json
└typings.json

npm start で TypeScript の自動コンパイルとアプリケーションのリロードを有効にしておこう。 このコマンドは TypeScript のコンパイラを watch mode で動かし、サーバーを動作させる。 アプリケーションをブラウザで起動し、開発中にアプリケーションを動作させたまま更新を反映させることができる。

ヒーローのサービスを作る

ステークホルダーが今のアプリを拡大していく見通しを我々に明かした。 彼らは、いろいろなページで、いろいろな形式でヒーローの情報を表示したいと言う。 今までの開発で、我々は一覧画面からヒーローを選択できるようになった。 この先、トップヒーローを表示するダッシュボード画面と、ヒーローの情報を編集する別画面を追加することになる。 この3つの画面では、共にヒーローの情報を必要としている。

今、AppComponent の中に表示用のヒーロー一覧データが定義されている。 ここで2つ問題がある。 1つは、ヒーローのデータを定義するのはコンポーネントの責務ではない、ということ。 もう1つは、ここに書かれているヒーローのデータを、別のコンポーネントや画面と共有するのが簡単ではない、ということだ。

そこで、ヒーローのデータを提供し必要なコンポーネント同士で共有できるように、 ヒーローのデータを取得するという仕事を1つのサービスに再構築することができる。

HeroService を作る

app フォルダに hero.service.ts というファイルを作る。

サービスを記述するファイルには .service を付けると言う決まりにしよう。 もしサービスの名前が複数単語で構成されるようなものであるなら、- で繋ごう(ケバブケース)。 例えば、SpecialSuperHeroService というサービスを定義するなら、そのファイル名は special-super-hero.service.ts となるだろう。

ここでは、サービスのクラス名を HeroService とし、 これを他のコンポーネントからインポートできるよう、エクスポートする。

import {Injectable} from 'angular2/core';

@Injectable()
export class HeroService {
}

注入可能なサービスとは

ここで、Angular の Injectable をインポートして、それをつかい @Injectable() というデコレーターで(HeroServiceを)修飾している、ということに気をつけてほしい。

後ろの()を忘れてはいけない。付け忘れてしまうと、ややこしいエラーが出て訳が分からなくなるぞ。

TypeScript はこの @Injectable() デコレーターを見て、 HeroService にあるメタデータを発行する。 そのメタデータとは、Angular がこのサービスを必要とされるコンポーネントに**注入(Injection)**する必要があるかもしれない、ということだ。

デコレータを付けたが、今のところは HeroService は何からも依存されていない。 これは、データの一貫性や、将来にわたっての互換性維持を最初から整えておくためのもので、 それは @Injectable() デコレーター適用についての「ベストプラクティス」であるのだ。

ヒーローのデータを得る

getHeroes という名前の**スタブ(stub)**メソッドを追加してみよう。

@Injectable()
export class HeroService {
  getHeroes() {
  }
}

今は中身の実装を保留しておくというのがポイントだ。

このサービスを使う「消費者」( = 各コンポーネント)は、このサービス自身がどうやって元となるデータを取得するかを知らないのだ。 HeroService はどこからデータを取得しても良い。 ウェブサービスでも、ローカルストレージでも、どこかにべた書きしたダミーデータでもいい。 これは、コンポーネントからデータアクセスの機能を取り除く、という美学である。 保留していた getHeroes に立ち返ると、この部分の実装は、データの取得方法に関して、コンポーネントに触ることなく、意のままに書いて良いということだ。

(注釈: getHeroes とは「どこからかヒーローのデータを持ってくる」というメソッドだ。この機能がサービスにあるということは、コンポーネントは「どこからかヒーローのデータを持ってくる」ことを考えなくて良くなる。またコンポーネントの責任が減るのだ。今実装を保留した部分は意のままに書いていい、ここを変えても、コンポーネントはそれを知らなくて良い、つまりコンポーネントに影響を与えないと言う点の美しさについてしつこく語っている。例えば、ヒーローのデータをテスト用のデータにしたいときにここを触るだけでよくなる)

ダミーのヒーロー達

さて、今ダミーのヒーロー一覧のデータが AppComponent に書かれている。 HeroService には属していない。 このデータを独自のファイルに移動しよう。 app フォルダに mock-heroes.ts を作り app.component.ts からヒーローの配列をカット&ペーストしよう。 import {Hero} from './hero'; のインポート宣言は、この配列が Hero 型であるために必要なものだ。

import {Hero} from './hero';

export var HEROES: Hero[] = [
    {"id": 11, "name": "Mr. Nice"},
    {"id": 12, "name": "Narco"},
    {"id": 13, "name": "Bombasto"},
    {"id": 14, "name": "Celeritas"},
    {"id": 15, "name": "Magneta"},
    {"id": 16, "name": "RubberMan"},
    {"id": 17, "name": "Dynama"},
    {"id": 18, "name": "Dr IQ"},
    {"id": 19, "name": "Magma"},
    {"id": 20, "name": "Tornado"}
];

HEROES の配列をエクスポートし、外部、例えば HeroService からインポート出来るようにする。 (注釈: ここ原文だと配列が constant って書いてあるけど実は変数だし嘘っぽいので気をつける)

そして app.component.ts に戻り、 AppComponentheroes プロパティを初期化しないようにしておく。

heroes: Hero[];

ダミーのヒーローデータを返すようにする

HeroService に戻って、ダミーの HEROESをインポートし、その配列を返すように getHeroes メソッドを書こう。 HeroService はこうなる。

import {HEROES} from './mock-heroes';
import {Injectable} from 'angular2/core';

@Injectable()
export class HeroService {
  getHeroes() {
    return HEROES;
  }
}

(疑問: ここで Hero 型のインポートがないのはなぜ? mock-heroes からの型推論でメソッドの型までちゃんと とHero 型であると認識される?、例えばメソッドに型を明示したら怒られるのかな?)

サービスを使う

まずAppComponent から、コンポーネント内で HeroService を使う準備をする。 まずいつものように使いたい HeroService をインポートしよう。

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

サービスをインポートしたことで、コンポーネントのコードから参照可能になった。 ここで、AppComponentは具体的な HeroService のインスタンスを取得するのだろうか。

HeroService を new すればいいのか?まさか!

たしかに、以下のように宣言すればインスタンスを得られる。

heroService = new HeroService(); // それ以上いけない

しかし、これはいくつかの理由で良くない方法なのだ。

コンポーネントは HeroService を作り出す方法を知る必要がある。 HeroService の**コンストラクタ(constructor new する時の初期化メソッド)**が変更されるたびに (たとえばメソッドに引数が追加されるとか)我々はサービスを作っている部分を全て探して書き換えなければならなくなる。 そのように書き換えが激しくなってしまったコードはエラーが発生しやすく、またテストの手間も掛かる。

サービスを毎回 new で作ることをもう少し考えてみよう。 このサービスが取得したデータを内部に溜め込んで、それを他の部分に使い回してくれるとしたら? それだと毎回 new することで台無しになってしまう。

このように、HeroService の特定の実装によっては AppComponent の実装をも束縛してしまうのだ。 状況が変化したときに実装を変更するのは大変になるだろう。 たとえばオフラインアプリケーションになったら?テストで別のデータを取り込むようにするべき時は?きっと簡単ではない。

こんなとき………どうすれば………いや、できることはある! 実のところ、上のような間違った書き方をする言い訳ができないほど、この問題の回避は簡単なのだ。

HeroService を注入する

一行で new する代わりに、以下の2つの処理に置き換える。

  • AppComponent にコンストラクタを追加する
  • コンポーネントのメタデータに providers プロパティを追加する

コンストラクタはこうだ。

constructor(private _heroService: HeroService) { }

このコンストラクタ自体はなにもしない。 このパラメーター定義は、_heroService というプライベートなプロパティを作り、それを注入対象の HeroService であると定義する。

ここで、このHeroService 型のプライベート変数が、コンポーネントのパブリックなAPIではないということを強調するため、変数名の先頭に _ を付けておこう。

これで、Angular は AppComponent が作られたときに、そこに HeroService のインスタンスを提供する方法を知ることができる。 Angular は、インスタンスをどこからか取得する必要がある、それは、Angular の DIコンテナ(Dependency Injector) の役割だ。 DIコンテナは(原文だとInjectorなので改めて説明すると)内部にコンテナを持っており、ここに作ったサービス(のインスタンス)を保持してくれる。 つまり、必要になったときに、コンテナに HeroService のインスタンスが入っていればそれを Angular に返すし、入っていなければ自動でインスタンスを作ってコンテナに入れた後にそれを Angular に返すのだ。

注入については、依存性の注入の章で、より深く学べるだろう。

DIコンテナはまだ HeroService の作り方を知らない。アプリケーションを実行したとき、Angular はこのようなエラーを出すだろう。

EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)

ここで、HeroService の提供者をコンポーネントに登録することで、DIコンテナに何が必要かを伝える必要がある。

@Component デコレータの中に書かれたメタデータの最下部に、providers という名前で配列プロパティを追加しよう。

providers: [HeroService]

この配列プロパティによって、Angular に対して AppComponent を作るときに HeroService の新しいインスタンスを作るよう、伝えることができる。 AppComponent はこのサービスを通して、ヒーローのデータを取得することができる他、子コンポーネントも同じサービス(の同じインスタンス)を通して、ヒーローのデータを取得することができる。

サービスとコンポーネントツリー

AppComponent がテンプレートの下部に書かれた <my-hero-detail> で、対応する HeroDetailComponent のインスタンスを作り出す、ということを思い出してほしい。 ということは、 HeroDetailComponentAppComponent の**子コンポーネント(child)**だということだ。

HeroDetailComponent が親コンポーネントの HeroService が必要なとき、AppComponent でしたときと同じように、コンストラクタ経由で Angular に HeroService を注入してもらう必要がある。

constructor(private _heroService: HeroService) { }

ただし、HeroDetailComponent のデコレータに providers を追加する必要はない! なぜかは付録で説明する。 AppComponent はアプリケーションで最も上位のコンポーネントだ。 つまり AppComponent のインスタンスは必ず1つであり、HeroService のインスタンスもアプリケーションを通して1つである。

AppComponentgetHeroes メソッドを作る。

今、AppComponent のプライベート変数_heroServiceHeroService のインスタンスが入っている。 これを使ってみよう。 一瞬考えてみよう。サービスの呼び出しとデータの取得は、合わせて1行で表現できる。

this.heroes = this._heroService.getHeroes();

本当はこの1行をラップするようなメソッドは必要ないのだが、以下のように書いておこう。

  getHeroes() {
    this.heroes = this._heroService.getHeroes();
  }

ngOnInit でアプリのライフサイクルを検知する

AppComponent は難なく(without a fuss)ヒーローの情報を取得し、表示する必要がある。 getHeroes はどこで呼ばれるべきだろうか?コンストラクタ内部?そうではない。

プログラミングの長い苦しみの歴史とは、我々に「コンストラクタ内部からサーバーへのデータアクセスのような複雑なロジックを絶対に入れてはいけない」という教訓を残した。

コンストラクタは与えられたパラメータをプロパティにセットするなど軽い初期化処理のためにある。ヘビーリフティングの為にあるのではない。 我々は、テストでもコンポーネントを作れる状態にする必要があり、そこで実際にやるであろう(サーバー呼び出しなどの)作業を(コンストラクタの中で)気にすることがないようにする必要もある。

され、コンストラクタの中で getHeroes を呼ばないのであれば、外部の何かがメソッドを呼ばなければならない。 Angular システム内に用意された ngOnInit というライフサイクルフック(Lifecycle Hook) の中身を実装をすれば、Angular はそのメソッドを呼び出してくれる。 Angular は他にも、コンポーネントのライフサイクルに関して、重要な瞬間を活用するためのフックインターフェースを用意している。 コンポーネントが作られた時のフック、変更が起こったときのフック、コンポーネントが最終的に破棄される時のフックなどだ。 そうしたインターフェースは、それぞれ1つのメソッドを持っており、コンポーネントの中でそのメソッドの実装を記述すると、Angular は適切なタイミングで処理を実行してくれるのだ。

ライフサイクルフックについては、ライフサイクルフックの章で、より深く学べるだろう。

OnInit インターフェースに関しての大筋は以下のようになる。

import {OnInit} from 'angular2/core';

export class AppComponent implements OnInit {
  ngOnInit() {
  }
}

ngOnInit メソッドに初期化するためのロジックを書いて、Angular に正しいタイミングで呼び出すのを任せてみよう。 今回で言えば、getHeroes を内部で呼んで、ヒーローの情報を初期化する処理だ。

  ngOnInit() {
    this.getHeroes();
  }

意図した通り、ヒーローの一覧が表示され、名前をクリックしたヒーローの詳細が表示される。 ゴールにだいぶ近づいた。しかしちょっと違うのだ。

非同期サービスと Promise

HeroService はダミーのヒーローデータを即座に返す。getHeroes のシグネチャは同期的だ。 (メソッドシグネチャはメソッドの定義を示す語としてよく使われている)

this.heroes = this._heroService.getHeroes();

ヒーローのデータを問い合わせて、それは返却値の中に入ってくる。 いつか、このヒーローのデータをサーバーから取得するようにするだろう。 まだ HTTP による問い合わせはしないが、後の章でやることになる。

そうしたときに、我々はサーバーからの応答を待たなければならない。 しかし、ブラウザの動作はブロックを許さない(UIの処理を止めてしまうと「応答がありません、このタブを閉じますか?」とか出てしまう)のだから、応答を待ってる間にUIの処理を止めたかったとしても止めてはいけない。

我々はgetHeroes メソッドの内容を書き換え、非同期処理を使うようにしなければならない。 ここで Promise を使う。

HeroServicePromise 対応にする

Promise とは……結果が準備できたときに(結果を使った)処理をコールバックすることを約束することだ。

我々は非同期サービスに対して、何らかの仕事をしてもらうとともに、コールバック関数を渡すことになる。 そうすると、何らかの仕事(サービスの機能)を行った後、その結果や出てきたエラーをもとに、渡したコールバック関数を実行してもらえる。

簡単にするために、ここでは ES2015 の Promise について説明するが、 他の言語やフレームワークの Promise については web の他のリソースで学んでほしい。

HeroServicegetHeroesPromise を返すように書き換えてみよう。

getHeroes() {
  return Promise.resolve(HEROES);
}

ヒーローのデータは未だダミーである。 これをサーバーとして想定すると、要求にタイムラグなしで応答するとても速いサーバーで、Promise はヒーローのダミーデータを すぐさま解決された状態で返却するだろう。

Promise で動くようにする

AppComponentgetHeroes メソッドは以下のようになっているはずだ。

  getHeroes() {
    this.heroes = this._heroService.getHeroes();
  }

HeroService に変更を加えた結果 this.heroes にはヒーローの一覧データではなく、Promise オブジェクトがセットされてしまうようになっている。

我々は、渡された Promise オブジェクトが解決されたときに動くように実装を変えなければならない。 Promise オブジェクトが解決されたときに、ヒーロー一覧が表示されるようにするのだ。

Promise が持つ then メソッドに、以下のコールバック関数を入れて渡そう。

getHeroes() {
  this._heroService.getHeroes().then(heroes => this.heroes = heroes);
}

コールバックとして渡しているES2015 のアロー関数は、それと等価な関数表現をより簡単に、より優雅に表現する。

コールバック関数は AppComponentheroes プロパティにサービスから返された一覧データをセットする。これで全てが揃った!

見た目上ではアプリケーションはヒーロー一覧を出し、選択したヒーローの詳細が出る、というのは変わらないはずだ。

サーバーの応答が遅いような場合を想定したヒーローのデータ取得が遅いバージョンを確認してみよう。

アプリの構造を再確認

この章でリファクタリングをした後のフォルダ構造を確認してみよう。

angular2-tour-of-heroes
├app
│├app.component.ts
│├hero.ts
│├hero-detail.component.ts
│├hero.service.ts
│├main.ts
│└mock-heroes.ts
├node_modules ...
├typings ...
├index.html
├package.json
├styles.css
├tsconfig.json
└typings.json

この章で議論に挙がったファイルはこちら

今回のまとめ

この章で我々が得たものは、以下のようにまとめられる。

  • 多くのコンポーネント間で共有できるサービスを作る
  • AppComponent が作られたときにヒーローのデータを取得するよう、ngOnInit というライフサイクルフックを使う
  • HeroServiceAppComponent のプロバイダとして登録する
  • ヒーローのダミーデータを作成し、それをサービスにインポートする
  • サービスが Promise を返すようにし、コンポーネントは Promise 経由でデータを取得するようにする

今回作ったもののデモ

次回

このアプリケーションはコンポーネントやサービスの共有によりより再利用性が高まっている。

我々はダッシュボード画面を作り、画面を移動するためのリンクを作り、そしてテンプレートの中でデータを整形して表示したい。 この先のアプリケーションの進化の過程で、そのような構成を簡単に作り成長させる方法、そしてメンテナンスの方法を学ぶことになるだろう。

Angular のルーター(Router) について、そして画面間を移動できるような設計に関して、次回作業を行いながら説明していく。


付録

もしヒーローのデータ取得が遅かったら?

ヒーローのデータがサーバーにあると考えて、サーバーとの接続が遅い場合を考えてみよう。

HeroService 型に Hero 型をインポートして、getHeroesSlowly メソッドを作ってみよう。

getHeroesSlowly() {
  return new Promise<Hero[]>(resolve =>
    setTimeout(()=>resolve(HEROES), 2000) // 2 seconds
  );
}

だいたいは getHeroes と同じだ。しかし、 Promise はヒーローのダミーデータを解決する前に2秒館待つようにしている。

AppComponent に戻り、_heroService.getHeroes を使っている部分を _heroService.getHeroesSlowly を使うように変えてみて、アプリケーションがどのような動作をするか見てみよう。

親コンポーネントのサービスが見えなくなる

AppComponent で注入された HeroService が子コンポーネントである HeroDetailComponent でも注入されるとき、HeroDetailComponent側ではprovidersプロパティの設定をしてはいけない というやり取りを行った。

なぜだろうか? 実はそうしてしまうと、 Angular は HeroDetailComponent のために新しい HeroService のインスタンスを作ってしまう。 HeroDetailComponent は独自のインスタンスは不要で、本当に必要なのは AppComponent の中のHeroService なのだ。 それでも providers プロパティの設定を行い HeroService のインスタンスを作ってしまうと、AppComponent の中の HeroService のインスタンスを隠してしまう。 (注釈: 参照を上書きしてしまい、AppComponent 側に実体があっても、HeroDetailComponent 側で作られたインスタンスが優先的に参照されるようになる)

provider プロパティへの登録は「どこにすべきか」を注意深く考えよう。 登録のスコープを理解して、間違ったコンポーネントの階層にサービスを作ってしまわないよう気をつけよう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment