くじら公園

プログラミングなど学んだことについて書きます

Promiseアンチパターン

Promise Anti-patternsを翻訳させて頂きました。著者のtaoofcodeから許可を頂いて翻訳、投稿しています。



Promiseは一度理解してしまえば簡単だが、いくつか頭を抱えさせるパターンがある。ここにあるのは私が経験したいくつかのアンチパターンだ。

ネストされたPromise

君は複数のPromiseをネストする:

loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
                    DoSomethingOnThem(something, another);
    });
});

君がこれをする理由は、両方のPromiseの結果で何かをする必要があるからだ。then()は一つ前のPromiseの結果しかコールバックに渡せないのでここでチェインを用いることはできない。

だが、君がこうする本当の理由は君がall()メソッドを知らないからだ。

修正は:

q.all([loadSomething(), loadAnotherThing()])  
    .spread(function(something, another) {
        DoSomethingOnThem(something, another);
});

よりシンプルになった。q.allによって返されるPromiseは、2つのPromiseの結果の配列として解決されてthen()に渡される。spread()メソッドはこの配列をコールバックの仮引数に対して分配する。

壊れたチェイン

君は以下のようなコードを書く:

function anAsyncCall() {  
    var promise = doSomethingAsync();
    promise.then(function() {
        somethingComplicated();
    });

    return promise;
}

ここでの問題はsomethingComplicated()メソッドの中で発生したエラーを捕まえることができないことだ。Promiseはチェインされることを意図されて作られている、そのためそれぞれのthen()呼び出しは新しいPromiseを返却する。次のthen()はこの新しいPromiseに対しコールすべきだ。一般的に最後のコールはcatch()にする、そうすればチェインのどこで発生したエラーでもcatch()の中でハンドルできる。

上のコードでは君が最後のthen()の結果の代わりに最初のPromiseを返した時点でチェインが壊れている。

修正は:

function anAsyncCall() {  
    var promise = doSomethingAsync();
    return promise.then(function() {
        somethingComplicated()
    });   
}

常に最後のthen()の結果を返すべきだ。

コレクションのバカ騒ぎ

君の手元には要素の配列があり、要素個々に対し何かを非同期に実行したいとする。君は再帰を用いた以下のようなコードを書いている自分に気付く。

function workMyCollection(arr) {  
    var resultArr = [];
    function _recursive(idx) {
        if (idx >= resultArr.length) return resultArr;

        return doSomethingAsync(arr[idx]).then(function(res) {
            resultArr.push(res);
            return _recursive(idx + 1);
        });
    }

    return _recursive(0);
}

あぁ、なんて非直感的なコードか。問題はいくつチェインすることになるか分からないときのメソッドのチェインの仕方にある。map()reduce()を思い出そう。

修正は:

q.allはPromiseの配列を引数にとり、結果の配列に解決してくれる。q.allを使えば以下のように、簡単に非同期呼び出しをそれらの結果にマップできる:

function workMyCollection(arr) {  
    return q.all(arr.map(function(item) {
        return doSomethingAsync(item);
    }));    
}

再帰を使った(非)解決策と違って、このコードは全ての非同期呼び出しを並列に開始する。当然、時間的により効率的だ。

もしPromiseを連続に実行する必要があれば、reduceを使う。

function workMyCollection(arr) {  
    return arr.reduce(function(promise, item) {
        return promise.then(function(result) {
            return doSomethingAsyncWithResult(item, result);
        });        
    }, q());
}

最高に綺麗なコードではないけれど、間違いなくより綺麗なコードだ。

ゴーストPromise

非同期に何かをするメソッドと、同期に処理するメソッドがある。君は2つのコードを一貫して処理するために、その必要がないときでもPromiseを作成する。

var promise;  
if (asyncCallNeeded)  
    promise = doSomethingAsync();
else  
    promise = Q.resolve(42);

promise.then(function() {  
    doSomethingCool();
});

最悪のアンチパターンというわけではない、しかし絶対的に分かりやすい方法があるー「値かPromise」をQ()でくるめば良い。このメソッドは値かPromiseを引数にとって適宜処理する。

Q(asyncCallNeeded ? doSomethingAsync() : 42)  
    .then(
        function(value){
            doSomethingGood();
        })
    .catch( 
        function(err) {
            handleTheError();
        });

注釈:私は最初ここでQ.whenを使う方法を提示した。ありがたいことにKris Kowal*1がコメントでこれを訂正してくれた。Q.whenではなく、Q()を使うーこちらのほうがより明快だ。

過度に鋭利なエラーハンドラ

thenメソッドは引数を2つとる、fulfilledハンドラとrejectedハンドラである。君は以下のようなコードを書くかもしれない:

somethingAsync.then(  
    function() {
        return somethingElseAsync();
    },
    function(err) {
        handleMyError(err);
});

このコードの問題はfulfilledハンドラの中で発生したエラーがエラーハンドラに渡されないことだ。

修正は、別のthenの中でエラーハンドリングするようにすれば良い:

somethingAsync  
    .then(function() {
        return somethingElseAsync();
    })
    .then(null,
        function(err) {
            handleMyError(err);
        });

またはcatch()を用いて:

somethingAsync  
    .then(function() {
        return somethingElseAsync();
    })
    .catch(function(err) {
        handleMyError(err);
    });

こうすればチェインの中で発生したどんなエラーでもハンドリングされる保証がある。

忘れられたPromise

君はPromiseを返却するメソッドを呼び出す。でも君はこのPromiseのことを忘れて君自身のPromiseを作成する。

var deferred = Q.defer();  
doSomethingAsync().then(function(res) {  
    res = manipulateMeInSomeWay(res);
    deferred.resolve(res);
}, function(err) {
    deferred.reject(err);
});

return deferred.promise; 

このコードはPromiseの効果ーシンプルであることを全く利用できていない。たくさんの無意味なコードが記述されている。

修正は、単純にPromiseを返却するようにする。

return doSomethingAsync().then(function(res) {  
    return manipulateMeInSomeWay(res);
});

*1:Qの作者