二つのプログラミングモデル

非同期IOメソッドを使う の章では http.clientRequest,http.clientResponse それぞれのオブジェクトのイベントを通じて非同期メソッドの処理を学習しました.

node.js では通常の手続き型のプログラミングモデルのように

var a = procedure1();
var b = procedure2(a);
var c = procedure3(b);
if( c ){
  var b = procedure4();
}
...
...

のような逐次実行のプログラミングモデルとは少し異なり,イベント同士を連鎖させていくことによって,IO待ち時間でも効率的に処理の進むプログラムを組み上げていくことになります.

ある関数を呼び出すときにその関数内で発生するイベントを連鎖させる方法は大きく分けて二つあります.

  • イベントの終了時に別の関数をコールバック関数として呼び出す方法
  • イベントを専用オブジェクトに定義し、関数の返り値として返す方法

コールバックモデル

「イベントの終了時に別の関数をコールバック関数として呼び出す方法」では,関数の引数に関数オブジェクトを取り,関数内で発生するイベント時にその関数オブジェクトを呼び出します.

非同期IOメソッドを使う では、download 関数内でいくつかのイベントが発生していました.ダウンロードの終了を通知するために notify 関数を Global に定義していましたが, これは良策ではありません.download 関数を別のところで使いたいときに困るでしょう?

この問題を解決するには、notify関数自体を引数として渡せるように download 関数のインターフェースを定義してあげることです.おおよそ次のような形で関数を定義することになるでしょう.

function download(url, callback){
   // ... (snip) ...

   request.on('response', function(response){
      // ... (snip) ...

      response.on('end', function(){
         callback && callback(response, body);
      });
   });
}

呼び出し側は次のようになるでしょう.

function notify(response, body){
   // ... (snip) ...
};

download(url, notify);

この方法のよいところは、シンプルであることです.次のように記述していけば,一連のイベント処理の流れがコールバックチェインで構成されることがわかります.

foo(arg0, function(arg1){
   bar(arg1, function(arg2){
      hoge(arg2, function(arg3){
         // ... (snip) ...
      });
   });
});

ただし,インデントが深くなったり,()や{}の対応を取るのに困るかもしれません.また,状況に応じた複数の種類のコールバック関数が必要なケースでは,コールバック関数に渡す引数が複雑になりがちです.このようなケースでは次に記述するイベント駆動モデルが適切でしょう.

イベント駆動モデル

「イベントを専用オブジェクトに定義し、関数の返り値として返す方法」を使うことで,複雑なイベント連鎖のケースに対応します.

イベント専用オブジェクトは events.EventEmitter と呼ばれるクラスのインスタンスです.このオブジェクトは次のような利用を想定しています.

  • instance.emit(‘eventName’, arg0, arg1, arg2, ...) でイベントを発火させる.
  • 関数の返値として instance を返す.
  • 関数の呼び出し側は instance.on(‘eventName’, function(arg0, arg1, arg2, ...){ }) でイベントハンドラを登録し,イベント発火時の挙動を定義する.

‘eventName’ は自由に定義することができます. http.clientRequset クラスや http.clientResponse クラスはそれぞれ独自のイベントを持っていますが,これもEventEmitterの機能を使って実装されています [1]

実際に, download 関数で実装する場合は次のようになるでしょう.

var EventEmitter = require('events').EventEmitter;
function download(url){
   var ev = new EventEmitter();

   request.on('response', function(response){
      // ... (snip) ...
      response.on('end', function(){
         ev.emit('notify', response, body);
      });
   });

   return ev;
}

呼び出し側はこのようになります.

var ev = download(url);
ev.on('notify', function(response, body){
   ...
});

イベント駆動で記述する場合は,コールバックモデルに比べて次の利点があるでしょう.

  • 異なるイベント名をつけることで,それぞれの状況に応じた引数を組み立てることができます.
  • イベント受信側では複数のイベントハンドラを登録することができます.尚,イベントハンドラが全く定義されていない場合に該当イベントが発火されると例外が発生します.独自のイベントを定義する場合は,きちんとインターフェースとしてイベントの名称および引数を記述する必要があります.

Note

node.js の世界の空気

ここまで,説明のためにコールバック関数やイベント名を独自に定義してきました.しかし,いずれのモデルでプログラムを記述するにしても,その世界の慣習は重要視すべきです.2010年末現在,明示的なルールがあるわけではありませんが,多くのライブラリを見ると以下の慣習が見え隠れします.

  • コールバック関数は関数の最後に与える

    function(arg0, arg1, arg2, ..., options, callback)
    
  • コールバック関数の第1引数はエラーオブジェクトにする

    callback(err, arg0, arg1, ...){
        if(err){
        }
    }
    
  • エラー発生時のイベント名は’error’にする

    try{
      // ...
    }catch(e){
      ev.emit('error', e);
    }
    
  • IOの途中でイベントデータを発火させる場合は,’data’ という名前にする

  • IOの終了時にイベントデータを発火させる場合は,’end’ という名前にする

[1]継承構造をたどっていくと EventEmitter クラスにたどり着きます.

Table Of Contents

Previous topic

非同期IOメソッドを使う

Next topic

ライブラリを作る

This Page