タイトル: シングルトン
SEOタイトル: Singleton パターン完全ガイド (Java / C# / Python / PHP の実装と落とし穴)
| この記事の要点 |
|
Singleton パターンとは
Singleton は GoF (Gang of Four) のデザインパターンの 1 つで、あるクラスのインスタンスがプロセス内で常に 1 つだけであることを保証するパターンです。設定オブジェクト、ロガー、DB コネクションプール、キャッシュなど「全体で 1 つあれば十分」な対象に使われます。
Singleton の本質は次の 2 点:
- インスタンスが 1 つしか作られないことを保証
- そのインスタンスへのグローバルアクセス手段を提供
基本的な実装 (Java)
public class AppConfig {
// 1. 唯一のインスタンスを保持する static フィールド
private static AppConfig instance;
// 2. 外部から new させないため private コンストラクタ
private AppConfig() {
// 設定ファイル読み込み等
}
// 3. 取得用 static メソッド
public static AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
public String getDbUrl() { return "jdbc:..."; }
}
// 使用
AppConfig cfg = AppConfig.getInstance();
String url = cfg.getDbUrl();
これは シングルスレッド前提の基本形。マルチスレッドでは 2 つのスレッドが同時に if (instance == null) を通過し、インスタンスが 2 つできるバグがあります。
スレッドセーフな実装 (Java)
方式 1: synchronized メソッド (シンプルだが遅い)
public static synchronized AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
方式 2: Double-Checked Locking
public class AppConfig {
private static volatile AppConfig instance; // ★ volatile 必須
private AppConfig() {}
public static AppConfig getInstance() {
if (instance == null) { // 1 回目チェック (ロック無し)
synchronized (AppConfig.class) {
if (instance == null) { // 2 回目チェック (ロック内)
instance = new AppConfig();
}
}
}
return instance;
}
}
方式 3: 静的初期化 (eager)
クラスロード時にインスタンス生成。Java のクラスローダがスレッドセーフを保証:
public class AppConfig {
private static final AppConfig INSTANCE = new AppConfig();
private AppConfig() {}
public static AppConfig getInstance() {
return INSTANCE;
}
}
方式 4: Initialization-on-demand Holder (Lazy + スレッドセーフ)
public class AppConfig {
private AppConfig() {}
private static class Holder {
static final AppConfig INSTANCE = new AppConfig();
}
public static AppConfig getInstance() {
return Holder.INSTANCE;
}
}
方式 5: Enum Singleton (推奨)
Joshua Bloch "Effective Java" で推奨されている方法。リフレクション・シリアライズ攻撃にも耐性:
public enum AppConfig {
INSTANCE;
private final String dbUrl = "jdbc:...";
public String getDbUrl() { return dbUrl; }
}
// 使用
String url = AppConfig.INSTANCE.getDbUrl();
C# の実装
.NET 4+ では Lazy<T> がベストプラクティス:
public sealed class AppConfig
{
private static readonly Lazy<AppConfig> lazy =
new Lazy<AppConfig>(() => new AppConfig());
public static AppConfig Instance => lazy.Value;
private AppConfig() { }
public string DbUrl => "jdbc:...";
}
// 使用
var cfg = AppConfig.Instance;
Python の Singleton (module-level)
Python では モジュール自体がシングルトン。import された結果はキャッシュされ、再利用されます:
# config.py (このモジュール全体がシングルトン)
db_url = "postgres://..."
api_key = "secret"
def get_db_url():
return db_url
# main.py
import config
print(config.get_db_url()) # どこでインポートしても同じインスタンス
クラスとして実装したい場合は __new__ でインスタンス管理:
class AppConfig:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._init()
return cls._instance
def _init(self):
self.db_url = "postgres://..."
a = AppConfig()
b = AppConfig()
print(a is b) # True
# デコレータ版
def singleton(cls):
instances = {}
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class Logger:
def log(self, msg): print(msg)
PHP の Singleton
class AppConfig
{
private static ?AppConfig $instance = null;
private string $dbUrl;
private function __construct() {
$this->dbUrl = getenv('DB_URL');
}
// クローン禁止
private function __clone() {}
// unserialize 禁止
public function __wakeup() {
throw new Exception("Cannot unserialize singleton");
}
public static function getInstance(): AppConfig {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getDbUrl(): string {
return $this->dbUrl;
}
}
// 使用
$cfg = AppConfig::getInstance();
echo $cfg->getDbUrl();
Singleton のメリット
- インスタンスが 1 つに限定される → 状態の一貫性
- グローバルアクセスで呼び出しが簡単
- メモリ消費抑制 (大きな初期化コストを 1 回だけ)
- 初期化の遅延 (Lazy) でアプリ起動が高速化
Singleton のデメリット (アンチパターン論)
| 問題 | 説明 |
|---|---|
| グローバル状態 | テストごとに状態が引きずられ テスト困難 |
| Mock しづらい | static 経由のため DI でモック差し替えにくい |
| 密結合 | AppConfig.getInstance() を直接呼ぶコードは AppConfig に強く依存 |
| 並列実行で衝突 | マルチスレッドで競合、ロック設計を間違うとデッドロック |
| サブクラス困難 | インスタンス管理が静的なため、継承で振る舞いを変えにくい |
| 初期化順 | 他の static フィールドと依存関係があると順序問題 |
現代的な代替: DI コンテナ
Spring (Java) / Laravel (PHP) / NestJS (TypeScript) など現代の DI コンテナは、Singleton スコープでインスタンスを管理します。Singleton パターン自体を手書きする機会は激減しました。
// Spring
@Service // デフォルトで Singleton スコープ
public class AppConfig {
public String getDbUrl() { return "..."; }
}
@Component
public class UserService {
private final AppConfig config;
// コンストラクタインジェクション
public UserService(AppConfig config) {
this.config = config;
}
}// Laravel
$this->app->singleton(AppConfig::class, function () {
return new AppConfig();
});
// 使用
$cfg = app(AppConfig::class);
// コントローラでインジェクト
public function index(AppConfig $config) {
return $config->getDbUrl();
}
DI コンテナの利点:
- テスト時に Mock 差し替えが容易
- ライフサイクル管理 (Singleton / Request / Transient) を切り替えやすい
- コードに
getInstance()が散乱しない - 初期化順を自動解決
FAQ
Q: Singleton はアンチパターンと聞いた
A: グローバル状態を作ること自体に問題があるという批判ですが、ロガー・設定・コネクションプール等の用途では今でも妥当。DI コンテナ + Singleton スコープなら問題の多くが解決します。
Q: シングルトンが複数できることはある?
A: ① 複数のクラスローダ (Java)、② リフレクションで private コンストラクタを呼ぶ、③ シリアライズ→デシリアライズ、④ クローン、⑤ マルチプロセス (プロセスごとに別インスタンス)、で破られます。Enum Singleton はリフレクション攻撃に耐性があります。
Q: Singleton と static class はどう違う?
A: 状態を持たない純粋関数なら static、状態・インターフェース実装・継承可能性を持たせるなら Singleton。詳細は別記事「static クラスとシングルトンの違い」へ。
Q: テストでシングルトンの状態が引き継がれて困る
A: テストごとに reset する resetForTest() メソッドを作るか、DI コンテナでスコープを Request にして毎回リセット。