無名関数を使った非同期通信のススメ(JavaScript)
2010.01.17
ここ最近はブラウザーの上で動く思いっきりRIAなアプリケーションを書いている私。こと通信の部分になると JavaScript での開発効率が、C++/Java/Objective Cなどと比べて格段に高いことをつくづく感じている毎日なので、今日は、そのあたりを少し解説してみようかと思う。
サーバーのAPIにアクセスするプログラムを書く方法は色々とあるが、「サーバー上の特定のURLにHTTPでアクセスして結果をXMLやHTMLやJSONで受け取る」というケースに限定すれば、基本的に3つのパターンに分けられる。
1. 同期通信
result = urlfetch.fetch("http://www.google.com/")
if result.status_code == 200:
doSomethingWithResult(result.content)
その書きやすさのために、実務経験の浅いプログラマーに好まれるのがこのパターン。上の例を見ても分かるように、「プログラムは上から順番に一行づつに実行して行く」というコンセプトとマッチしているので、プログラムを書くのも簡単だし、読むのも楽だ。それに加えて、すべてのコンテキスト(ローカル変数だとか、コールスタックだとか)が保持されたままなので、関数の中でサーバーから取得したデータをさらに加工して、それを関数の戻り値として返す、などが自然にできる。
このパターンの一番の問題は、実行スレッドが通信中に解放されないことである。UIスレッドからこんなプログラムを実行すればユーザーインターフェイスが無反応になってしまうし(初期の Microsoft Outlook が典型的な例)、シングルスレッドのアプリケーションから複数の通信を同時にすることが不可能になる。
「それを回避するためにマルチスレッドがある」と言う人もいるが、ほとんどの場合その考え方は誤りである。安易な考えからマルチスレッドを導入したためにデッドロックに苦しんだり、スレッドを作りすぎてパフォーマンスが極端に落ちる、などの失敗例は星の数ほど見て来た。MicrosoftのDCOMも、当初の実装は「プロセス間通信のたびに一つスレッドを作りそれに同期通信をさせる」というアーキテクチャだったため、ExcelからWordに何かをCut&Pasteするたびにスレッドがそれぞれのプロセスに7個ずつ作られる、という悲惨なものであった。
ちなみに、この「通信をバックグラウンドでさせるためにスレッドを作る」という過ちは、ある程度経験を積んだプログラマでも犯しやすい間違いなので注意した方が良い(プログラミングの入門書などでそんなパターンを奨励しているものまであったりするので困ったものだ)。
2. 非同期通信(コールバック・オブジェクト方式)
そんな同期通信の欠点を補うために導入されたのが、非同期型の通信である。
NSURL* url = [NSURL URLWithString:"http://www.google.com/"];
NSURLRequest * request = [NSURLRequest requestWithURL:url ...];
MyDelegateClass* delegateObject = [MyDelegateClass ...];
NSURLConnetion* connection = [NSURLConnection connectionWithRequest:request
delegate:[delegateObject];
...
// MyDelegateClass.m ファイル
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data {
doSomethingWithResult(data);
}
これは、Objective Cの例(iPhone OS)だが、connectionWithRequest:delegate:メソッドは、単に「通信の開始」を指示するだけで、サーバーからのデータをまたずにすぐ処理を戻す。そして、実際の通信は非同期にバックグラウンドで行なわれ、通信結果はコールバック用に渡したオブジェクト(delegateObject)の connection:didReceiveData:メソッドに非同期に渡される。
この仕組みを使えば、たとえUIスレッドからこのコードを実行しても、ユーザーインターフェイスがロックすることもないし、複数の通信を平行して同時に実行することも可能になる。
このパターンの一番の欠点は、プログラムが非常に読みにくく(つまりメンテナンスしにくく)なる点である。delegateObjectのメソッドは、上のコードとは離れたところ(多くの場合、別のファイル)に書かれており、「処理を目で追う」ことがとても難しくなる。コンテキストもdelegateObjectを介して明示的に渡さねばならず、同期通信では3〜4行のコードで書けていたものが、非同期通信を使うと新しくクラスを導入した上で複数のファイルにまたがる百行を越すコードを書かねばならない、などということはしばしばある。
私が、WindowsやiPhone OS上でC++やObjective Cで書いたプログラムは、基本的にはすべてこのパターンで書かれているが、そのたびに「プログラミング効率の悪さ」にイライラしてきたことも事実である。
3. 非同期通信(無名関数)
上の非同期通信プログラミングの持つ「プログラミング効率の悪さ」を一気に解消してくれるのが、クロージャと無名関数をサポートする言語(JavaScriptやRuby)である。
$.get("http://www.google.com/", null, function(data, textStatus) {
doSomethingWithResult(data);
});
このパターンの利点は、上の例(JavaScript上でjQueryを使って通信をしている例)を見ただけで一目瞭然である。非同期通信でありながら、同期通信と同じく、何をやろうとしているかが一目で分かる。クロージャを使えば、新たなクラスなど導入せずに、コンテキストを渡すことも容易である(クロージャ内の変数に直接アクセスするだけで良い)。上の二つのパターンの良いところを持ち合わせたのが、このパターンだ。
◇ ◇ ◇
以上が、このエントリーの冒頭で「こと通信の部分になると JavaScript での開発効率が、C++/Java/Objective Cなどと比べて格段に高いことをつくづく感じる」理由だ。「JavaScriptプログラマーは、クロージャと無名関数が自在に使いこなせるようになって、やっと一人前」と言われる理由もここにある。
非常に興味深い記事を多く書かれてらっしゃるので、
ありがたく拝見させていただいています。
突然ですがお願いがあります。
私は現在、ICTに関わる卒業論文を書いているのですが、
2009.12.06の「GoogleはなぜAndroidやChrome OSを無料で配布するのか?」
という記事で、私の考えることをより分かりやすく、より深く
考察していらっしゃったので、もし宜しければ卒業論文に同様の図などを
使わせていただけないでしょうか。
メールアドレスなど見つけられませんでしたので
こちらに書き込みさせていただきました。失礼いたしました。
Posted by: 大学4年生 | 2010.01.21 at 07:34
Snow LeopardにはBlockが導入されて3の無名関数が利用できるようになったので、飛躍的にコードの記述性が高まりましたね。昨年のWWDCのセッションでも、コードの読みやすさ、保守の観点からブロックを導入することのメリットを強調していました。
早くiPhone SDKでも使えるようになってもらいたいものです。
Posted by: Basuke | 2010.02.14 at 23:06