singleton-pattern について
JavaScript 開発者向けに、クラスのインスタンスを一つに制限し、モジュール間で共有する Singleton パターンを解説します。グローバルな可変状態、テスト、依存関係の隠蔽に関する課題も説明します。
主な機能
- 01クラスのインスタンスを一つに制限
- 02モジュール間でインスタンスを共有
- 03エクスポートしたインスタンスを凍結
- 04テストとグローバル状態のリスクを解説
活用シーン
- 01アプリ全体で設定やリソースを共有
- 02複数モジュールでカウンター状態を管理
- 03テスト容易性への影響を評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill singleton-patternインストール後、Claude Code で/singleton-patternを入力して読み込まれたか確認します。
詳細
シングルトンパターン
目次
シングルトンは、一度だけインスタンス化でき、グローバルにアクセスできるクラスです。この 単一のインスタンス はアプリケーション全体で共有できるため、シングルトンはアプリケーションのグローバル状態の管理に適しています。
使用する場合
- アプリケーション全体で共有するクラスのインスタンスが正確に1つだけ必要な場合に使用します
- グローバル状態、設定、共有リソースの管理に役立ちます
使用しない場合
- クラスのインスタンスを複数必要とする場合 — シングルトンは設計上、共有インスタンスを1つに限定します
- コードのテストや把握を難しくする、隠れたグローバル状態の結合が生じる場合
- 依存性注入やモジュールスコープの変数で、よりテストしやすく同じ結果を得られる場合
手順
- コンストラクターで既存のインスタンスを確認し、作成できるインスタンスが1つだけになるようにします
- エクスポートするインスタンスに
Object.freeze()を使用して、誤った変更を防ぎます - JavaScript では、可能な場合、クラスベースのシングルトンよりもシンプルなオブジェクトリテラルやモジュールを優先します
- React では、グローバル状態にシングルトンを使うよりも、状態管理ツール(Redux、Context)を優先します
- 可変状態が共有されるため、シングルトンによってテストが難しくなることがある点に注意してください
詳細
まず、ES2015 のクラスを使ってシングルトンがどのようなものになるか見てみましょう。この例では、次の機能を持つ Counter クラスを作成します。
- インスタンスの値を返す
getInstanceメソッド counter変数の現在の値を返すgetCountメソッドcounterの値を 1 増やすincrementメソッドcounterの値を 1 減らすdecrementメソッド
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
しかし、このクラスはシングルトンの要件を満たしていません。シングルトンは一度しかインスタンス化できないはずです。現状では、Counter クラスのインスタンスを複数作成できます。
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
console.log(counter1.getInstance() === counter2.getInstance()); // false
new メソッドを 2 回呼び出すと、counter1 と counter2 には異なるインスタンスが設定されます。counter1 と counter2 の getInstance メソッドが返す値は、実質的に異なるインスタンスへの参照です。つまり、厳密には同じではありません。
Counter クラスのインスタンスを 1 つだけ作成できるようにしましょう。
インスタンスを 1 つだけ作成できるようにする方法の 1 つは、instance という変数を作成することです。Counter のコンストラクターでは、新しいインスタンスが作成されたときに、instance にそのインスタンスへの参照を設定できます。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 counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!
これで、複数のインスタンスを作成できなくなりました。
counter.js ファイルから Counter のインスタンスをエクスポートしましょう。ただし、その前にインスタンスも凍結する必要があります。Object.freeze メソッドを使うと、利用するコードがシングルトンを変更できないようにできます。凍結されたインスタンスのプロパティは追加も変更もできないため、シングルトンの値を誤って上書きするリスクが軽減されます。
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;
次のファイルを使って Counter の例を実装するアプリケーションを考えてみましょう。
counter.js:Counterクラスを含み、Counterインスタンスをデフォルトエクスポートとしてエクスポートしますindex.js:redButton.jsモジュールとblueButton.jsモジュールを読み込みますredButton.js:Counterをインポートし、Counterのincrementメソッドを赤いボタンのイベントリスナーとして追加します。また、getCountメソッドを呼び出してcounterの現在の値をログに出力しますblueButton.js:Counterをインポートし、Counterのincrementメソッドを青いボタンのイベントリスナーとして追加します。また、getCountメソッドを呼び出してcounterの現在の値をログに出力します
blueButton.js と redButton.js は、どちらも counter.js から同じインスタンスをインポートします。両方のファイルで、このインスタンスは Counter としてインポートされます。
redButton.js または blueButton.js のどちらかで increment メソッドを呼び出すと、Counter インスタンスの counter プロパティの値は両方のファイルで更新されます。赤いボタンと青いボタンのどちらをクリックしても、すべてのインスタンスで同じ値が共有されます。そのため、異なるファイルでメソッドを呼び出しているにもかかわらず、カウンターは 1 ずつ増え続けます。
トレードオフ
インスタンス化を 1 つの インスタンスに限定すると、メモリを大幅に節約できる可能性があります。毎回新しいインスタンス用にメモリを確保する代わりに、アプリケーション全体で参照されるその 1 つのインスタンス用にメモリを確保するだけで済みます。しかし、シングルトンは実際には アンチパターン と見なされており、JavaScript では避けることができます(あるいは、避ける べき です)。
Java や C++ など、多くのプログラミング言語では、JavaScript のように直接オブジェクトを作成できません。こうしたオブジェクト指向プログラミング言語では、オブジェクトを作成するクラスを定義する必要があります。作成されたオブジェクトは、JavaScript の例における instance の値と同じように、そのクラスのインスタンスの値を持ちます。
しかし、上記の例で示したクラスの実装は、実際には過剰です。JavaScript ではオブジェクトを直接作成できるため、通常のオブジェクトを使うだけで、まったく同じ結果を得られます。シングルトンを使うことの欠点をいくつか見ていきましょう。
通常のオブジェクトを使う
先ほどと同じ例を使います。ただし今回は、counter は次の要素を含む単なるオブジェクトです。
countプロパティcountの値を 1 増やすincrementメソッドcountの値を 1 減らすdecrementメソッド
オブジェクトは参照渡しされるため、redButton.js と blueButton.js は、どちらも同じ counter オブジェクトへの参照をインポートしています。どちらかのファイルで count の値を変更すると、counter の値が変更され、その変更は両方のファイルから確認できます。
テスト
シングルトンに依存するコードのテストは、難しくなることがあります。毎回新しいインスタンスを作成できないため、すべてのテストは前のテストによるグローバルインスタンスへの変更に依存します。この場合、テストの順序が重要となり、わずかな変更によってテストスイート全体が失敗する可能性があります。テスト後は、テストによる変更をリセットするために、インスタンス全体をリセットする必要があります。
依存関係が隠れる
別のモジュール(ここでは superCounter.js)をインポートするとき、そのモジュールがシングルトンをインポートしていることがすぐには分からない場合があります。ここでは index.js などの別のファイルで、そのモジュールをインポートしてメソッドを呼び出すことがあります。その結果、シングルトンの値を誤って変更してしまう可能性があります。アプリケーション全体で複数の箇所からシングルトンが共有されている場合、それらすべての箇所にも変更が反映されるため、予期しない動作につながることがあります。
グローバルな振る舞い
シングルトンのインスタンスは、アプリ全体から参照できる必要があります。グローバル変数も基本的に同じ振る舞いをします。グローバル変数はグローバルスコープで利用できるため、アプリケーション全体からアクセスできます。
一般に、グローバル変数を使うのは悪い設計判断だと考えられています。グローバルスコープを汚染すると、誤ってグローバル変数の値を上書きしてしまい、予期しない動作を引き起こす可能性があります。
ES2015 では、グローバル変数の作成はあまり一般的ではありません。新しい let と const キーワードは、これらのキーワードで宣言した変数をブロックスコープにすることで、開発者が誤ってグローバルスコープを汚染するのを防ぎます。JavaScript の新しい module システムでは、モジュールから値を export し、ほかのファイルでその値を import できるため、グローバルスコープを汚染せずに、グローバルにアクセス可能な値を簡単に作成できます。
しかし、シングルトンの一般的なユースケースは、アプリケーション全体で何らかのグローバル状態を持つことです。コードベースの複数の部分が同じ変更可能なオブジェクトに依存すると、予期しない動作につながることがあります。
通常、コードベースの特定の部分がグローバル状態内の値を変更し、それ以外の部分がそのデータを利用します。ここでは実行順序が重要です。まだ利用できるデータがないのに、誤って先にデータを利用してしまうことは避けたいものです。アプリケーションが成長し、何十ものコンポーネントが互いに依存するようになると、グローバル状態を使ったデータフローの把握は非常に難しくなることがあります。
React における状態管理
React では、シングルトンを使う代わりに、Redux や React Context などの状態管理ツールを通じてグローバル状態を扱うことがよくあります。これらのツールのグローバル状態の振る舞いはシングルトンと似ているように見えるかもしれませんが、シングルトンの 変更可能な 状態ではなく、読み取り専用の状態を提供します。Redux を使う場合、コンポーネントが ディスパッチャー を通じて アクション を送信した後に、純粋関数である リデューサー だけが状態を更新できます。
これらのツールを使っても、グローバル状態を持つことの欠点がなくなるわけではありませんが、コンポーネントが状態を直接更新できないため、少なくとも意図した方法でグローバル状態が変更されるようにできます。
出典
参考資料
- React Hooks は Redux に取って代わるのか - Eric Elliott
- JavaScript デザインパターン:シングルトン - Samier Saeed
- シングルトン - Refactoring Guru