アプリケーション開発において、「このデータは、システム全体でただ一つだけにしたい」という要件は頻繁に発生します。例えば、サイト全体の設定情報や、ログインしているユーザーのセッション情報などがそれに当たります。
このような「アプリでたった一つだけのインスタンス」を保証するための設計手法が「シングルトンパターン」です。
このシングルトンパターンには、歴史的背景や技術の進化に伴い、主に2つの実装方法が存在します。
- クラシックなシングルトンパターン(従来の実装方法)
- モジュールシングルトン(ES6 modulesの特性を活用した手法)
この記事では、この2つの実装方法とそれぞれの特徴、そして現代の開発におけるより良いアプローチは何かを比較・検討してみることにしました。
注意: シングルトンパターンは、グローバルな状態を生み出し、コードの依存関係を複雑にすることから「アンチパターン」と見なされることも多いです。この記事では実装方法を比較しますが、採用する前には本当に必要かどうかを慎重に検討することをお勧めします。
クラシックなシングルトンパターン
まず、従来から知られている実装方法です。この方法は、1994年に出版された名著『オブジェクト指向における再利用のためのデザインパターン』(通称: GoF本)で紹介された、非常に歴史のある伝統的な手法です。
class ClassicSingleton {
private static instance: ClassicSingleton;
// `new`でのインスタンス化を禁止するため、コンストラクタをprivateにする
private constructor() {
console.log('Classic Singleton instance created');
}
// インスタンスを取得するための静的メソッド
public static getInstance(): ClassicSingleton {
// インスタンスがまだなければ生成し、あれば既存のものを返す
if (!ClassicSingleton.instance) {
ClassicSingleton.instance = new ClassicSingleton();
}
return ClassicSingleton.instance;
}
}
// 常にgetInstance()経由でアクセスする
const instance1 = ClassicSingleton.getInstance();
const instance2 = ClassicSingleton.getInstance();
console.log(instance1 === instance2); // true
※画像やファイルは記事投稿時に別途挿入します
クラス自身がインスタンスを管理し、getInstance()
メソッドを通じてのみアクセスを許可する、という点が特徴です。
モジュールシングルトン(ES6 modules活用型)
次に、ES6(ECMAScript 2015)で導入されたモジュールシステムの特性を活用した手法です。JavaScriptのモジュールは「一度だけ評価(実行)される」という仕様になっており、この仕組みが自然とシングルトンを実現してくれます。
class MyConfig {
// こちらはごく普通のクラス
constructor() {
console.log('Module Singleton instance created');
// 設定の読み込みなどを行う
}
public getConfig() {
return { theme: 'dark' };
}
}
// モジュールのトップレベルでインスタンスを生成してエクスポートする
export const configInstance = new MyConfig();
利用する側は、特別なメソッドは必要なく、ただimport
するだけです。
import { configInstance } from './ModuleSingleton';
// どのファイルからimportしても、configInstanceは常に同じインスタンス
const config = configInstance.getConfig();
console.log(config.theme); // 'dark'
特徴の比較
両者の違いを一覧表にまとめてみました。
項目 | クラシックなシングルトン | モジュールシングルトン |
---|---|---|
インスタンス制御 | クラス内部で制御 | ESモジュールで制御 |
アクセス方法 | getInstance() |
直接import |
遅延初期化 | ✅ 可能 | ❌ モジュール読み込み時 |
テストでのモック | ❌ 困難 | ✅ 簡単 |
依存関係注入 | ❌ 困難 | ✅ 可能 |
TypeScript親和性 | ⚠️ 普通 | ✅ 高い |
注意点
モジュールシングルトンは多くの点で優れていますが、万能ではありません。シングルトンパターンそのものが持つ課題と、代替案についても触れておきます。
シングルトンの課題・注意点
- 隠れた依存関係:
import
やgetInstance()
でどこからでも手軽に呼び出せる反面、どのクラスがそのインスタンスに依存しているのかが分かりにくくなります。 - テストの難しさ: グローバルな状態を持つため、テストケース間で状態が汚染しあう可能性があります。モジュールシングルトンはモックしやすいですが、根本的な問題は残ります。
- 設定の動的変更: モジュールシングルトンは読み込み時にインスタンスが生成されるため、実行環境に応じて設定を動的に切り替えるといったことが難しくなります。
代替案
最近の開発では、シングルトンよりも柔軟でテストしやすい以下の手法が推奨されることが多いです。
1. 依存性注入(Dependency Injection, DI)
必要とするオブジェクトを外部から注入する設計です。これにより、依存関係が明確になり、テスト時にはモックオブジェクトを簡単に注入できます。
// ApiClientという依存オブジェクトをコンストラクタで受け取る
class UserService {
constructor(private apiClient: ApiClient) {}
async getUser(id: string) {
return this.apiClient.get(`/users/${id}`);
}
}
2. 関数型アプローチ
必要な設定を受け取ってオブジェクトを返す純粋な関数(ファクトリ関数)を用意する方法です。
// 設定管理オブジェクトを作成する関数
export const createConfigManager = (env: NodeJS.ProcessEnv) => ({
apiUrl: env.NODE_ENV === 'production'
? 'https://api.prod.com'
: 'http://localhost:3000'
});
// 使用時に環境変数を渡して設定オブジェクトを作成
const config = createConfigManager(process.env);
まとめ
今回は、2種類のシングルトンパターンと、その代替案について比較しました。どれを選ぶべきか、私なりの推奨度をまとめてみました。