📕

Singleton Patternの基礎理解

に公開

はじめに

https://www.patterns.dev/#patterns
で学んだことを理解のためにアウトプットする。

シングルトン(Singleton)とは

そのクラスからは絶対に1つしかインスタンスを作れないようにする仕組み
のこと。例えるならば、
会社に社長は1人だけ、部署に部長は1人だけ、みたいなもの。
唯一無二の存在。
シングルトンは、一度インスタンス化してしまえば、グローバルにアクセスできるクラスである。そのため、この単一インスタンスはアプリケーション全体で共有することができるという利点もある。

Counterクラスの例

ES2015のクラスを使って、例として以下のメソッドを持つCounterクラスを作成する。
※ES2015(ECMAScript 2015)というJavaScriptの標準化仕様

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

この書き方だと、Counterクラスのインスタンスを何個も作成できてしまう
一度しかインスタンス化できないというシングルトンの基準を満たさない!

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

newメソッドが2回呼ばれて、counter1counter2に異なるインスタンスがセットされてしまう。両者のgetInstanceメソッドが返してくる値は、異なるインスタンスの参照となるため、===ではなくなってしまう。
これは、「一つの会社に社長が2人いる」みたいな状態になってしまいおかしい。

解決策:インスタンスを1つに制御する

instanceという変数を宣言して、Counterのコンストラクタで新たにインスタンスが作成されたときに、そのインスタンスへの参照をinstanceにセットしてしまうというものがある。

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

この手法であれば、インスタンスがすでに作成されている状態から、新たにインスタンスを作成しようとした場合にエラーが発生してユーザーが検知できるようになる。

Object.freezeで完全固定

このファイルからCounterのインスタンスをエクスポートする。
このとき、事前にインスタンスを 凍結(freeze) しておく必要がある。凍結する際には、Object.freezeメソッドを使用する。
これにより、凍結したインスタンスに対して追加・変更などができなくなるため、間違ってシングルトンの値を上書きしてしまうといったリスクを低くすることができる。

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

部分で凍結している。全体のコードは下記。

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

シングルトンのメリット・デメリット

メリット

・メモリを節約できる
・アプリケーション全体で同じ値を共有することができる

デメリット

・テストが複雑になる
・依存関係が隠蔽されてしまう
・コードの実行順によって挙動が変わったり、意図しない変更が起きたりする

Reactでの状態管理

Reactにおいては、シングルトンではなく、ReduxReact Contextなどの状態管理ツールを介してグローバル状態を利用することが一般的である。これらのツール動作はシングルトンと似ているが、シングルトンのような可変状態ではなく、読み取り専用(readOnly)の状態を提供している。
つまり、

state.count = 100; // ❌ 直接変更できない

みたいなことはできない。

どうやって更新するか?

Reduxを使用する場合は、コンポーネントがdispatcherを介してactionを送信したのちに、純粋関数であるreducerのみが状態更新できる。

action: {
  type: "INCREMENT",
  payload: { count: 0 }
}
function reducer(state, action) {
  if (action.type === "INCREMENT") {
    return { count: state.count + 1 };
  }
  return state;
}

というような流れ以外で、状態(state)を変えることはできない!

これらのツールを使うことで、グローバルな状態をもつことの欠点が消えるわけでは無いが、コンポーネントが状態を直接更新できないことによって、少なくともグローバルな状態が意図したとおりに変更されるようにできる。

参考

https://www.patterns.dev/vanilla/singleton-pattern/
https://zenn.dev/miumi/articles/ac6c648f17bc6f

Discussion