Singleton: initialize()
対象の Singleton
派生クラスを初期化します。
引数として関数を与えることで、追加の初期化処理を記述できます。
構文
singleton.initialize(initializer) => Singleton
引数
-
initializer
:function
|undefined
対象の
Singleton
派生クラスを初期化する関数です。 引数initializer
として指定された関数は、引数を持たず、また返値も利用されません。引数
initializer
として関数が指定された場合、それは対象のSingleton
派生クラスが未初期化である場合にのみ呼び出されます。この引数の指定は任意です。指定がない場合、対象の
Singleton
派生クラスは初期化済みとして扱われますが、初期化処理としては何も行われません。初期化関数にはアロー関数を渡しましょう-
初期化関数内では、初期化したい
Singleton
派生クラスのインスタンスをthis
を介して操作することになります。 JavaScript の言語仕様上、function
式では明示的にthis
を指定して呼び出さない限り、function
式を書いたスコープにおけるthis
は利用されません(クラス定義の中ではthis
の値がundefined
になります)。代わりに、常にアロー関数式を使うべきです(アロー関数式は記述位置でのthis
を取り込むため、期待通り初期化したいSingleton
派生クラスのインスタンスを参照します)。// 誤った例 class Misused extends Singleton { constructor() { super(); return this.initialize(function() { // ここでの this は Misused のインスタンスではなく undefined を指します // 以下は undefined.x の参照になるためエラーを引き起こします this.x = 42; }); } } // 好ましい例 class Preferable extends Singleton { constructor() { super(); // アロー関数式の代わりに `(function() { ... }).bind(this)` でもよいですが、 // アロー関数の方が簡潔かつ bind() の書き忘れなどのおそれもないので、 // bind() を使う動機はすくないはずです。 return this.initialize(() => { // ここでの this は期待通り Preferable のインスタンスになります this.x = 42; }); } }
非同期処理にご注意ください- 引数
initializer
としてasync
関数やPromise
を返す関数を与えないでください。initialize()
関数は返値はinitializer
を値を返さない同期関数であるものとして扱い、Promise
が返されたとしても何も行いません。Promise
のthen
内(async
関数内でのawait
以降)の処理はマイクロタスクとして実行されますが、マイクロタスクの実行はイベントループに積まれたタスクの実行後に処理されます(詳細はMDNの『JavaScript で queueMicrotask() によるマイクロタスクの使用』 を参照)。
-
返値: Singleton
対象の Singleton
派生クラスのインスタンスです。
対象の Singleton
派生クラスが未初期化であれば、対象のインスタンスそのものが返ります。
初期化済みであれば代わりに、対象のインスタンスではなく、対象の Singleton
派生クラスの初期化済みのインスタンスが返ります。
この値は、対象の Singleton
派生クラスのコンストラクタの返値を上書きするために用います。
- JavaScriptにおいて、コンストラクタは暗黙に
this
を返しますが、一方で、明示的に値を返すこと(つまりreturn
文を書くこと)もできます。明示的に値を返すことを「返値の上書き (return overriding)」と呼びます。 - 返値の上書きをすることで、
new
演算子の結果やsuper()
呼び出し後に参照されるthis
の値を変えることができます。Singleton
派生クラスにおける利用では、新たに生成された未初期化のインスタンスの代わりに、初期化済みのインスタンスを返すために使います。 - 返値の上書きによって
this
と異なるオブジェクトを返した場合、それ以前に行ったthis
への操作は反映されません。return
されたオブジェクトとそれ以前にthis
で参照されていたオブジェクトが別のオブジェクトであるためです。- 通常のプログラムで上記のような混乱が生じることはありませんが、クラスのフィールドの定義などを行っている場合には注意が必要です。フィールドの定義は、
super()
の呼び出し後、その時点でのthis
に対して行われます。そのため、フィールド定義と返値の上書きを組み合わせると奇妙な振る舞いをすることがあります。
- 通常のプログラムで上記のような混乱が生じることはありませんが、クラスのフィールドの定義などを行っている場合には注意が必要です。フィールドの定義は、
例
以下ではファイル SharedData.js
の中でシングルトンパターンを実装する派生クラスとして、SharedData
クラスを定義しています。
class SharedData extends Singleton {
#data;
constructor() {
super();
// initialize() の結果で SharedData クラスのコンストラクタの返値を上書きします
return this.initialize(() => {
// initialize() の引数の中で初期化処理が行われます。
// この初期化関数は SharedData クラスの最初のインスタンス生成時にのみ
// 実行され、2回目以降のコンストラクタ呼び出しでは実行されません。
// 例示のため固定のデータ構造を初期化時に与えています。
this.#data = {
x: 42,
y: "foo"
};
});
}
// 値の取得用の関数
// データオブジェクトの変更不可能なコピーを取得し、
// 引数として与えた関数 block() で何らかの処理を行います。
// block の this および第一引数にはデータオブジェクトのコピーが与えられます。
async get(block) {
await this.#acquire();
// 例示のためネストしたオブジェクトは考慮していません。
const it = Object.freeze({ ...this.#data });
try {
const result = block.call(it, it);
if (result instanceof Promise) {
await result;
}
} catch(error) {
this.#release();
throw error;
}
this.#release();
}
// 値の更新用の関数
// データオブジェクトの変更可能なコピーを取得し、
// 引数として与えた関数 block() で何らかの処理を行います。
// block の this および第一引数にはデータオブジェクトのコピーが与えられます。
// コピーに対して加えた変更は元のデータオブジェクトに反映されます。
async update(block) {
await this.#acquire();
// 例示のためネストしたオブジェクトは考慮していません。
const it = ({ ...this.#data });
try {
const result = block.call(it, it);
if (result instanceof Promise) {
await result;
}
} catch(error) {
this.#release();
throw error;
}
for (const k of Object.keys(it)) {
if (!Object.hasOwn(this.#data, k)) { continue; }
const old_value = this.#data[k];
const new_value = it[k];
if (old_value !== new_value) {
this.#data[k] = new_value;
}
}
this.#release();
}
async #acquire() {
await Promise.race([this.#lock, this.#timer]);
const { promise: lock, resolve: release } = Promise.withResolvers();
lock.release = release;
const { promise: timer, resolve: timeout } = Promise.withResolvers();
timer.timeout = timeout;
setTimeout(() => {
timeout();
this.#timer = null;
}, 1000);
this.#lock = lock;
this.#timer = timer;
}
#release() {
this.#lock?.release();
this.#lock = null;
}
}
解説
対象の Singleton
派生クラスを初期化します。
引数として関数を与えることで、追加の初期化処理を記述できます。
この関数は、対象の Singleton
派生クラスが未初期化であれば、対象のインスタンスそのものを返します。
初期化済みであれば代わりに、対象のインスタンスではなく、対象の Singleton
派生クラスの初期化済みのインスタンスを返します。
この関数は必ず constructor()
の中で呼び出してください。また、この関数の返値は呼び出し元の constructor()
の返値の上書きのために使用してください(Singleton
派生クラスのコンストラクタの使用についてはSingleton
# コンストラクタも参照してください)。
Singleton
派生クラスでこの関数を呼び出さなかった場合、そのクラスは Singleton
クラスではありますが、シングルトンパターンを実装するクラスにはなりません。つまり、そのクラスのインスタンスはアプリケーション上に複数作ることができます。