この記事のポイント

  • シングルトン設計で発生しやすい設計ミスを整理
  • 生成責任・依存責任・ライフサイクル責任をレビューアー視点で読み解く
  • スレッドセーフ実装・DI設計・グローバル化肥大の回避技法を体系化

そもそもシングルトンとは

シングルトンは「システム全体でインスタンスを1つだけ持つ」ことを保証する設計パターンです。

  • 同一オブジェクト共有で一貫性を保証
  • 多重生成防止
  • コンフィグ管理、接続プール、ログ管理等で適用される

Pythonは言語仕様上インスタンス生成が非常に柔軟であるため、シングルトン設計が非常に崩壊しやすい特徴を持ちます。

最小例
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

なぜこれをレビューするのか

シングルトンは便利だが、以下の設計ミスが非常に多いです。

  • グローバル化による密結合化
  • スレッド安全性未保証
  • テスト困難化
  • 依存注入設計の破綻

レビューアーは生成責任と依存責任を分離整理する立場で読み解く必要があります。

レビューアー視点

  • 生成保証責任が集中管理されているか
  • スレッドセーフ性が保証されているか
  • ライフサイクル管理と依存性注入に配慮されているか
  • グローバル変数化・静的汚染を起こしていないか

開発者視点

  • シングルトンは設計整理が前提の構造設計責任であることを意識
  • DI(依存注入)と組み合わせて利用範囲制御を行う

良い実装例

正常設計例:スレッドセーフ責務集中
import threading

class ConfigLoader:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialize()
        return cls._instance

    def _initialize(self):
        self.config = {"env": "prod", "version": "1.0"}

良い理由

  • 生成責任が__new__に集中
  • スレッドセーフ (double-checked locking)
  • 外部コードにグローバル変数化せず提供可能
  • 初期化責任を別メソッドに分離

レビュー観点

  • __new__が生成責任を一元管理しているか
  • ロックによりスレッド安全が保証されているか
  • 外部への直接グローバル露出を避けているか
  • 初期化責任が生成直後に完全完了しているか

良くない実装例: ケース1

問題例: グローバル化による責務崩壊
class ConfigLoader:
    def __init__(self):
        self.config = {"env": "prod"}

config_loader = ConfigLoader()
@Reviewer
モジュールグローバルでインスタンスを生成・公開しています。依存箇所が全てこの変数に密結合化します。生成責任はクラス内部に集中させましょう。

問題点

  • モジュールグローバルで密結合化
  • テスト困難・依存性注入不能
  • 生成責任がコード全域に分散

改善例

改善後: クラス内に生成責任集中
class ConfigLoader:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance
設計補足
  • 呼び出し側が生成責任を持たずクラス集中
  • グローバル変数に頼らず依存制御が容易化

良くない実装例: ケース2

問題例: スレッド非安全
class ConfigLoader:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
@Reviewer
__new__内でロック保護なくインスタンス生成しています。多重生成競合の可能性があります。スレッドセーフ設計を必ず導入してください。
return cls._instance

問題点

  • 多重スレッド下で競合
  • 多重インスタンス生成リスク

改善例

改善後: ロック導入でスレッドセーフ化
class ConfigLoader:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance
設計補足
  • ダブルチェックロックにより生成競合排除
  • Pythonでも安全なスレッド下動作保証

良くない実装例: ケース3

問題例: テスト不能な密結合化
class Logger:
    _instance = None

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = Logger()
@Reviewer
呼び出し箇所全域でget_instance固定呼び出しとなり、テスト時にMock化困難化します。依存注入経路に整理しましょう。
return cls._instance

問題点

  • コード全体がLogger.get_instance()に固定化
  • モック差替え困難

改善例

改善後: 依存注入経路で疎結合化
class Logger:
    def log(self, message):
        print(message)

# 利用箇所
class Service:
    def __init__(self, logger: Logger):
        self.logger = logger
設計補足
  • シングルトン生成は呼び出し元で管理
  • クラスは依存注入でインスタンスを受け取るだけに整理

PlantUML設計イメージ

UML Diagram
UML Diagram

観点チェックリスト


まとめ

シングルトンは構造管理パターンであり「使い方が簡単」わけではありません。
レビューアーは生成責任・依存責任・ライフサイクル責任を静かに分解し、
「生成は制御するが依存は固定しない」設計構造へ誘導するレビュー技術が求められます。
依存注入と併用できるシングルトン設計は現場でも非常に強固な技法となります。