class X {
async init() {
}
}
みたいなPromiseがあったとき、これを一回だけ実行したい。普通に考えると、initの結果をキャッシュするメモ化を行いたくなる。たとえば、以下のコード。
// Bad Pattern
class X {
async init() {
if (!this.initResult) {
this.initResult = await this._init();
}
return this.initResult;
}
async _init() {
}
}
これだと、initの実行中に別のinitが開始されると複数回実行されてしまう。
こういう場合には、定石があって、
class X {
init() {
if (!this.initPromise) {
this.initPromise = this._init();
}
return this.initPromise;
}
async _init() {
}
}
とする。あまり変わらないようだけど、asyncが外れたことで、メモ化の対象がPromiseになったことがポイント。この場合、_initは必ず一回だけ実行される。Promiseは、pendingやresolved状態で、何度でもresolveできるので、この性質を使っている。自分は、最初、この挙動を理解するのに結構戸惑ったけど、最近やっと慣れてきた。
さて、実装としては、これで良いのだけど、このためだけに、_initとか、initPromiseとか本質的ではない名前が増えるのは嫌だ。そういうときのためにdecoratorというものがある。
function memoize(target, fieldName, descriptor) {
const func = descriptor.value;
const memoizedFieldName = Symbol(`$$once_${fieldName}`);
return Object.assign({}, descriptor, {
value: function() {
if (!this[memoizedFieldName]) {
this[memoizedFieldName] = func.apply(this);
}
return this[memoizedFieldName];
},
});
}
class X {
@once
async init() {
}
}
前のコードで、_initに相当するフィールドは、クロージャーに隠蔽され、initPromiseに相当するフィールドは、シンボルで隠蔽されてるので、Xの名前空間は何も汚染せず、整理整頓された気分になる。
onceという名前にしたけど、内部の実装はただのメモ化で、同期処理のメモ化にも使える。実務上は、以下のようなライブラリを使えば良い。
ちなみに、「非同期メモ化」で検索したら、4年前に自分が書いた記事が出てきた。
やりたいことは同じなんだけど、今回紹介した、Promiseを使ったやり方の方がエレガントでコード量も少ないので、前に書いたやり方を使うべき状況はないと思う。4年前にはPromiseはすでに一般的だったと記憶しているけど、その時点で、自分はPromiseのことを良く理解してなかったらしい。黒歴史だ。