shimada-kの日記

ソフトウェア・エンジニアのブログです

Node.jsでPromiseを使う場合

先日、東京Node学園12限目に参加しました。そこで@jovi0608さんから紹介があったNode-v0.12に含まれる予定のPromiseを試してみました。試してみて分かったことを書いておきます。

Node-v0.12の新機能について

コールバック地獄の例としてよく挙げられる下記のようなコードのようにディレクトリ間でファイル数とファイル名に差分が無いことを確認できればあとはファイルごとのループに書き換えられるような事例ではPromiseの恩恵が受けられます。

引用元:Node.jsにPromiseが再びやって来た!

fs.readdir(dir1, function(...) {
    fs.readFile(file1, function(...) {
        hash.update(data);
        fs.readdir(dir2, function(...) {
            fs.readFile(file2, function(...) {
                ...
            });
        });
    });
});

ただしMySQLを使用した以下のようなコードはWHERE句に直前のクエリの結果を用いているので、各クエリを非同期で実行できないためコールバック関数で順番に処理せざるを得ません。つまりこれ以上ロジックを変えられないためPromiseを使っても意味がありません。まあ当然です。

connection.query('SELECT * FROM user', function(err, rows, fields) {
    if (err) throw err;

    connection.query('SELECT * FROM user_country WHERE user_id = ' + rows[0].user_id, function(err, rows, fields){
        console.log(rows);
        connection.end();
    });
});

Promiseの恩恵を受けられるのは待ち合わせる目的のみでコールバック関数を使用している場合です。コールバック処理のネストが一掃されるわけではありません。

とはいえ、ディレクトリ操作以外にも外部APIへ複数回問い合わせに行き、それぞれの結果が出そろってから何かをするようなケースだとPromiseの恩恵を受けられるはずです。

例としてtwitterAPIを使用し、自分のPOSTをリツイートしたユーザIDを調査するロジックを実装しました。

twitterAPIはリクエストを投げまくるとHTTP#429(Too Many Requests)を返してよこすのでユーザ数を5としました)

promiseが実装されているNode.jsはgithubのmasterブランチに上がっているものを使用します(2014-05-10現在)。

shimada-k@debian:~/node_pjt/sample/chat$ node -v
v0.11.14-pre
shimada-k@debian:~/node_pjt/sample/chat$ node -e 'console.log(process.versions.v8)'
3.25.30

外部APItwitterAPIを使用し、twitterのNodeモジュールはnode-twitterを使用しました。

コールバックを使用した場合

// tweet_idのツイートをリツイートしたuser_idを返す
function getRetweeter(tweet_id) {
    twit.get('/statuses/retweeters/ids.json', {'id' : tweet_id}, function(retweeters) {
        if(retweeters){
            console.log(retweeters.ids);
            return retweeters.ids;
        }
        else{
            var err = Error("[/statuses/retweeters] APIアクセスエラー");
            throw err;
        }
    }); 
}

// 自分のtweetでリツイートされたものを返す
function getRetweeted() {
    var retweeter_ids = new Array();

    twit.get('/statuses/retweets_of_me.json', {'count' : '5'}, function(tweet) {
        if(tweet){
            for(var i = 0; i < tweet.length; i ++){
                console.log(tweet[i].text + '@' + tweet[i].id_str);

                try {
                    var retweeters = getRetweeter(tweet[i].id_str);
                    retweeter_ids.push(getRetweeter(tweet[i].id_str));
                } catch(e) {
                    console.log(e);
                }   
            }   
            //console.log(tweet);
        }   
        else{
            var err = new Error("[/statuses/retweets_of_me] APIアクセスエラー");
            throw err;
        }   
    }); 

    return retweeter_ids;
}

function getRetweeters() {
    try{
        var retweeted = getRetweeted();
        console.log(retweeted);
    } catch(e) {
        console.log(e);
    }   
}

getRetweeters();

Promiseを使用した場合

// tweet_idのツイートをリツイートしたuser_idを返す
function getRetweeter(tweet_id) {
    return new Promise(function(onFulfilled, onRejected) {
        //console.log(tweet_id);
        twit.get('/statuses/retweeters/ids.json', {'id' : tweet_id}, function(retweeters) {
            if(retweeters){
                console.log(retweeters.ids);
                onFulfilled(retweeters.ids);
            }
            else{
                var err = new Error("[/statuses/retweeters] APIアクセスエラー");
                onRejected(err);
            }
        });
    });
}

// 自分のtweetでリツイートされたものを返す
function getRetweeted() {
    var ids = new Array();
    return new Promise(function(onFulfilled, onRejected) {
        twit.get('/statuses/retweets_of_me.json', {'count' : '5'}, function(tweet) {
            if(tweet){
                for(var i = 0; i < tweet.length; i ++){
                    console.log(tweet[i].text + '@' + tweet[i].id_str);
                    ids.push(tweet[i].id_str);
                }   
                //console.log(tweet);
                onFulfilled(ids);
            }   
            else{
                var err = new Error("[/statuses/retweets_of_me] APIアクセスエラー");
                onRejected(err);
            }   
        }); 
    }); 
}

function getRetweeters() {
    return getRetweeted().then(function(ids) {
        return Promise.all(ids.map(function(id) {
            return getRetweeter(id);
        }));
    }).then(
        function(x) {
            console.log(x);
        },  
        function(err) { // all case of errors
            console.log(err.messages);
        }   
    );  
}

getRetweeters();

エラーの捕捉箇所が一元化されて大分見通しが良くなりました。