はじめに
どうも、Angular1についてはそれなりに詳しい自信があるものの、Angular2の話をするのはこれが初めてという童貞野郎 @Quramy です。
今日はAngular1とAngular2のHTTP関連サービスAPIの違いから、両者の非同期処理に対するスタンスの違いについて考察したいと思います。
Ajax実装コードの違いを見てみる
まずは、シンプルに下記のようなPetリソースを単純にGETで取得するTypeScriptコードをAngular1, Angular2のそれぞれで書いてみます。
export interface IPet {
id: string;
type: string;
name: string;
}
まずはAngular1のHTTPから。お馴染みの$httpサービスを利用します。
/// <reference path="../typings/angularjs/angular.d.ts" />
class AppCtrl {
pet: IPet;
constructor(private $http: ng.IHttpService) {
this.$http.get('/api/v1/pets/001.json')
.then(resposnse => response.data as IPet)
.then(pet => {
this.pet = pet;
console.log(this.pet);
// :
});
}
}
angular.module('app').controller('AppCtrl', AppCtrl);
続いてAngular2の場合。 angular2/http
モジュールに含まれるHttp
サービスを利用します。
import {Component} from 'angular2/angular2';
import {Http, HTTP_PROVIDERS, Response} from 'angular2/http';
@Component({
selector: 'ngcli-sample01-app',
providers: [HTTP_PROVIDERS],
templateUrl: ...
})
export class MyApp {
pet: IPet;
constructor(private http: Http) {
this.http.get('/api/v1/pets/001.json')
.map((response: Response) => response.json() as IPet)
.subscribe(pet => {
this.pet = pet;
console.log(this.pet)
// :
});
}
}
それぞれ、get
メソッドに以下の2つのコールバック関数が登録される、という構造は同じです。
(「普通はService作るだろ、コンポーネントから直接Httpなんて呼ばねーよ、このクズが」とかのツッコミは、サンプルということでご勘弁を)
- HTTP Response からbody部からJSON(IPet)を取り出す
- 受けとったIPet型のオブジェクトをゴニョゴニョする(大概はコンポーネントのメンバ変数に代入されたりする)
import
文やDecorators等、HTTP用のサービスを利用するまでの準備は多少違いますが、アプリケーションのコード中の主な違いは下記であることが分かると思います。
- Angular1の場合、
.then(...)
でコールバック関数を登録 - Angular2の場合、
.map(...)
と.subscribe(...)
にコールバック関数を登録
雰囲気で、.map
は レスポンスからオブジェクトを取り出す処理、.subscribe
はリスナの登録っぽい、ということは分かりますが、最初にAngular2のHttpサービスを触って思ったのは、「何で then(...)
じゃなくなったんだ?mapとかsubscribeとか何者だよ」という点です。
PromiseかObservableか、それが問題だ
Angular1の$httpサービスでAjaxを実行した結果は、全てng.IPromise
という型の戻り値を返却します。
実体は$qというサービスで生成されますが、ES6で仕様化されたPromiseと同様に扱うことができる代物でした。
一方、Angular2のHttpサービスが返却するのはPromiseではなく、Observable
というclassです。
Observableは, angular2/http
ではなく、コアモジュールの angular2/angular2
に含まれています。
その定義を見てみると、Angular2が独自に定義したものではなく、ReactiveX/RxJS であることが分かります。
export declare class Observable<T> extends RxObservable<T> {
lift<T, R>(operator: Operator<T, R>): Observable<T>;
}
import { Subject, Observable as RxObservable } from '@reactivex/rxjs/dist/cjs/Rx';
こうなるともはやHTTPやAjaxに閉じた話では無く、非同期処理全般に関連する話題といえるでしょう。
ようやく本エントリのタイトルに持ってこれました。
僕が最初に思った疑問「何故http.get(...).then
が無くなった?」すなわち「何故Angular2から$qが消えたのか?」の答え(但し浅い)としては「PromiseからReactiveX/RxJSに乗り換えたから」と言えるようです。
でも、これじゃぁ答えになっていません。
確かに冒頭のAngular2コードに登場した .map
, .subscribe
の使い方や、他にObservableにどのような機能があるかを知りたければ、ReactiveX/RxJSや、その源流であるRxJS Main Module Referrence を読めば理解できるでしょう。
(@armorik83 に指摘もらった通り、RxJSは2系統存在してます. Angular2は @ReactiveX
付きの方を利用)
本当に理解したいのは、「何でAngular2が変更するという選択を採ったか」という、その理由なのです。
この疑問に対する1つの答えとして、2015年10月に開催されたAngular Connectでの RxJS in-Depth by Ben Lesh を観ると、PromiseよりもObservableが優れている点について述べてくれています(5:48辺りから観ると丁度良い)。
説明の内容をかいつまんで書くと、以下のようになります。
モダンなWebアプリで扱うべき非同期処理には下記が挙げられるが、
- Dom events (0-N values)
- Animations (canselable)
- Ajax (1 value)
- WebSockets (0-N values)
- Server Side Events (0-N values)
Promiseの特徴(このシンプルな特徴がPromiseの良い所とも言えるが)をと照らし合わせると、
- 成功か失敗のかのどちらかになることが保証されている。一度生成したPromiseがキャンセルされることはない
- 一度決定した成功・失敗の結果は不変である
先に挙げた非同期処理のうち、Promiseが解決してくれるのは、Ajaxだけである。
一方、発生したイベントをストリームとして捉えることが出来るObservableでは、先述した非同期処理のどれでも対応可能である
RxJS開発者が語っているだけあってか、Promiseの印象悪くなっちゃいそう。。。
Promise・Observableのどちらも、非同期処理に付随するコールバック地獄から開発者を救うための手段となり得ますが、それぞれに一長一短あるのも事実です。
PromiseはFutureというデザインパターンの一種ですので、「処理を呼ぶ -> (必要であれば待つ) -> 結果を受けて次の処理に進む」を抽象化する目的が根底にあります。
だからこそ、先日TypeScript1.7から利用できるようになったAsync/Awaitのように、直列的に非同期を含むコードを書くような記法との相性が良い訳です。
「Promiseが何回もResolveされるので、thenに登録したコールバックが何回動くかは不明です」とか「Promiseはキャンセルされました」とか言われたら、Async/Awaitなんてとてもじゃないけど書けそうにありません。
一方、ObservableはPublisher/Subscriver モデルが源流です。
このモデルの最大の特徴は、「イベントの発生元(Publisher) とリスナ(Subscriver)を分離することによる疎結合性」ことが挙げられます。
ObservableはSubscriverに相当しますが、イベント発生元がHttp.get(url)
であれ何であれ、そんなことは知ったこっちゃありません。
この疎結合性がこそが、様々なイベントをソースとして扱える原動力でしょう。
Angular = {1: "two way", 2: "one way"}
;
最後に、Angular1とAngular2のデータバインディング思想の違いが、非同期実装の違いとどう関連しているかを考えたいと思います。、
一昨日のAdvent Calendar で @shinsukeimai さんも述べられていますが、Angular2ではデータバインディングの方法が柔軟になっています。
Angular1の最大の特徴であった双方向データバインディングはデフォルトではなくなり、[View -> Component], [Component -> View]という2種類の単方向バインディングの組み合わせで実現されるようになりました。
一方、Observable(Angular2)についても、先述したように、「非同期処理の発生源」と「結果処理」が独立しているので、[何か -> 非同期発生] と [何かしらの結果 -> その後の処理] という2つの単方向な存在がある、と先ほどのアナロジーで捉えることができます。
ここまで来ると、どこかで聞いたことのある話になってきました。 そう、FluxやReduxで言われている「全てが単方向処理」というやつですね。
Promise から Observableへの乗り換えも、単方向処理を軸に捉えることが出来そうです。
ちなみに双方向って本当に悪いことなんでしょうか?
Angular1における、双方向データバインディング、Promiseの「[呼び出し <-> 結果処理]のセット」は、確かにBen Leshが言うようにWeb Socketのように解決できないパターンもあるかもしれません。
でも、「inputとoutputを常に組み合わせとして扱う」という制約をコードを書く上で守る必然性から、それなりに見通しの良いコードにできます。
単方向処理やイベントとリスナの疎結合性は、個別のコンポーネントのテスタビリティ等は高いですが、気づいたら「どこで何が起こるか分からない」という事態に陥りやすいリスクを含んでいます。
FluxやReduxも、「どこで何をするか」をガイドライン化していることでこのリスクを回避しようとしています。
おわりに
$qがなくなったといっても、Observableには toPromise(...)
等の変換系ユーティリティもあるので、Angular1と同じようにPromiseをベースに非同期を扱うアプリケーションをAngular2で作ることも可能だと思います。
Angular2でRxJSの豊富な機能が使えるようになったことは喜ばしい反面、設計時に注意を要求しそうな予感を感じています。
僕はRxJs含め、所謂FRP的なパラダイムについては実装・設計ともに実務経験はないのですが、Angular2を契機にしてこいつらと向き合うのも楽しそうだな、と思っています。
今日は殆ど実装に触れない話となりましたが、いずれ設計 + 実装を両面をAngular2で試した上でもう一度このテーマを語ってみたいですね。
明日は @yuhiisk さんです。
参考
- Angular2 Observables, Http, and separating services and components : Angular2 Httpの使い方がより詳細に紹介されてます
- 【翻訳】あなたが求めていたリアクティブプログラミング入門 : 程よい粒度でReactiveについて解説してくれてる
- デザインパターン紹介 -GoF以外のパターンを紹介します- : そもそもFutureパターンって何だっけ?って話を結城先生が解説してくれてる