no versions found for ApplicationFrameworkReferences / Utility/singleton_initialize / en

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 が返されたとしても何も行いません。Promisethen 内(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 クラスではありますが、シングルトンパターンを実装するクラスにはなりません。つまり、そのクラスのインスタンスはアプリケーション上に複数作ることができます。