「訳」 Promiseのアンチパターン

✍🏼 作成日 2016年03月04日   
❗️ 注意:この記事が作成されてから既に 日が経過しています。情報の鮮度にご注意ください

最近Promise関連のものを読んでいて、この記事を見つけ、とても良いと思ったので記録しておきます。

Promises自体は非常にシンプルですが、前提としてその糸口を見つけられるかどうかです。以下はPromiseに関するいくつかの混乱しやすいポイントで、これらを理解することでPromiseを本当にマスターしているか確認できます。中には私自身も頭を抱えたものがあります。

ネストされたPromises

複数のPromisesが互いにネストされている状況です:

1
2
3
4
5
loadSomething().then(function (something) {
loadAnotherthing().then(function (another) {
DoSomethingOnThem(something, another);
});
});

このように書く理由は、2つのPromiseの結果を両方処理する必要があるためで、チェーン呼び出しができないからです。なぜならthen()メソッドは前のthen()が返す結果しか受け取らないためです(つまりこれら2つのPromiseは同時に処理される必要があり順序関係はありませんが、thenには順序関係があり、前者のthrow``errorが直接catch処理ステップに入る場合です)。

実はこのような書き方をする本当の理由は、all()メソッドを知らないからです:

この醜い書き方を解決する方法:

1
2
3
4
5
6
q.all([loadSomething(), loadAnotherThing()]).spread(function (
something,
another
) {
DoSomethingOnThem(something, another);
});

より簡潔になりました。q.all()promiseオブジェクトを返し、その結果を配列に結合してresolveメソッドに渡します。その後thenメソッドで使用され、spread()メソッドはこの配列を分割して複数の配列長のパラメータとしてDoSomethingOnThem関数に渡します。

(注:Promise.all()は配列をパラメータとして受け取り、配列要素はpromiseで、要素間に順序関係はなく同時に実行されます。最後にthenメソッドに渡される値は各promiseメソッドのreturn値の配列です。ここでは作者がnodeのモジュールqを例として使用しています)

中断されたチェーン呼び出し

次のようなコードがあるとします:

1
2
3
4
5
6
7
function anAsyncCall() {
var promise = doSomethingAsync();
promise.then(function () {
somethingComplicated();
});
return promise;
}

このコードの問題は、somethingComplicated()関数内で発生したerrorが捕捉されないことです。Promisesはチェーン呼び出しが可能であることを意味します(そうでなければthenと呼ぶ必要がなく、単にdoneで十分です)。各呼び出されたthen()メソッドは新しいpromiseを返し、この新しいpromiseは次のthen()メソッドで呼び出しが継続されます。通常、最後の呼び出しはcatch()メソッドで、チェーン呼び出しのどこかで発生したerrorはすべて捕捉・処理されます。

上記のコードでは、最初のpromiseを返すときにチェーン呼び出しが中断され、then処理後の新しいpromiseを最後のthen呼び出しに返していません(つまりthenは元のpromiseを変更せず、それを処理して新しいpromiseを返すだけです)。
この問題の解決策:

1
2
3
4
5
6
function anAsyncCall() {
var promise = doSomethingAsync();
return promise.then(function () {
somethingComplicated();
});
}

覚えておいてください。常に最後のthen()の結果を返すようにします(チェーン呼び出しが可能になるように)。

混乱したコレクション

要素の配列があり、各要素に対して非同期操作を実行したいとします。そのため、再帰呼び出しを含む処理が必要だと気づきます。

1
2
3
4
5
6
7
8
9
10
11
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);
}

うーん…このコードはあまり直感的ではありません。問題の核心は、チェーン呼び出しの長さがわからない場合、チェーン呼び出しが苦痛になることです。(JavaScript ES5+ネイティブの配列メソッド)map()reduce()を知らない限り。

解決策:
q.allパラメータはpromiseの配列であり、結果を配列にまとめてresolveメソッドに渡すことを覚えておいてください。配列要素のmapメソッドを使って各要素にこの非同期呼び出しメソッドを実行できます:

1
2
3
4
5
6
7
function workMyCollection(arr) {
return q.all(
arr.map(function (item) {
return doSomethingAsync(item);
})
);
}

最初の再帰呼び出しのような解決策ではない方法とは異なり、このコードは配列の各要素を同期的に非同期呼び出し関数に渡します。明らかに時間的により効率的です。
promisesを順番に返す必要がある場合は、reduceを使用できます:

1
2
3
4
5
6
7
function workMyCollection(arr) {
return arr.reduce(function (promise, item) {
return promise.then(function (result) {
return doSomethingAsyncWithResult(item, result);
});
}, q());
}

完全に簡潔とは言えませんが、最初のものよりは確かに整理されています。

ゴーストPromise

確定的なメソッド(Promiseの実行開始時にこのメソッドが与えられ、実行結果によって決定されるものではない)があり、時には非同期呼び出しが必要で、時には不要な場合があります。そのため、非同期と同期の場合でコードを一貫させるためだけにPromiseを作成します(抽象化と分離のため)、実際にはどちらか一方の状況しか発生しない場合でも。

1
2
3
4
5
6
var promise;
if (asyncCallNeeded) promise = doSomethingAsync();
else promise = Q.resolve(42);
promise.then(function () {
doSomethingCool();
});

上記のコードはアンチパターン中最も悪いものではありませんが、より明確に書くべきです—Q()valueまたはpromiseをラップします。Q()メソッドは値もpromiseもパラメータとして受け取ります:

1
2
3
4
5
6
7
Q(asyncCallNeeded ? doSomethingAsync() : 42)
.then(function (value) {
doSomethingGood();
})
.catch(function (err) {
handleTheError();
});

注記:最初はこの状況でQ.when()を使用することを提案していましたが、Kris Kowal氏のコメントのおかげで誤りから救われました。Q.when()は使用せず、Q()だけで十分です。後者の方がより明確です。

貪欲なエラーハンドラ

小見出しの意味は、thenfulfilledrejectedを同時に設定し、rejected関数を使ってthen関数の引数としても渡されるfulfilledのエラーを処理しようとするものですが、これは不可能です。fulfillederrorは次のthen()にしか渡されず、現在のrejected関数では処理されないため、この小見出しは『過剰な期待』とされています—エラー処理を切望しているものの、エラーは決して渡されず処理されることはありません—訳者注

then()メソッドは2つの引数を受け取ります。fulfilled状態を操作する関数とrejected状態を操作する関数です。以下のようなコードを書いたことがあるかもしれません:

1
2
3
4
5
6
7
8
somethingAsync.then(
function () {
return somethingElseAsync();
},
function (err) {
handleMyError(err);
}
);

この書き方の問題点は、fulfilled状態で発生したerrorがエラー処理関数に渡されないことです。
この問題を解決する方法は、エラー処理関数を独立したthenメソッド内で確実に実行させることです:

1
2
3
4
5
6
7
somethingAsync
.then(function () {
return somethingElseAsync();
})
.then(null, function (err) {
handleMyError(err);
});

またはcatch()を使用する方法もあります:

1
2
3
4
5
6
7
somethingAsync
.then(function () {
return somethingElseAsync();
})
.catch(function (err) {
handleMyError(err);
});

これにより、チェーン呼び出し中に発生するあらゆるerrorが確実に処理されます。

忘れられたPromise

あるメソッドを呼び出してpromiseを返しますが、このpromiseを忘れてしまい、さらにpromiseを作成してしまう場合:

1
2
3
4
5
6
7
8
9
10
11
12
var deferred = Q.defer();
doSomethingAsync().then(
function (res) {
res = manipulateMeInSomeWay(res);
deferred.resolve(res);
},
function (err) {
deferred.reject(err);
}
);

return deferred.promise;

このコードは本当にpromsieの簡潔さを完全に捨て去っています—無駄なコードが多すぎます。
解決策は、単にpromiseを返すだけです:

1
2
3
return doSomethingAsync().then(function (res) {
return manipulateMeInSomeWay(res);
});
- EOF -
この記事の初出: 「訳」 Promiseのアンチパターン - Xheldon Blog