#02 デザインパターン入門

Singleton——インスタンスをひとつに制限する

問題: 同じオブジェクトが何度も生成される

データベースコネクションや設定クラスは、アプリケーション全体でひとつだけ存在すれば十分です。しかしコードのあちこちで new DatabaseConnection() と書いていると、接続が何度も生成されてしまいます。

// 問題のあるコード
class UserRepository
{
    public function find(int $id): array
    {
        $db = new DatabaseConnection(); // 毎回新しい接続を生成
        return $db->query("SELECT * FROM users WHERE id = $id");
    }
}

class OrderRepository
{
    public function findByUser(int $userId): array
    {
        $db = new DatabaseConnection(); // ここでも新しい接続
        return $db->query("SELECT * FROM orders WHERE user_id = $userId");
    }
}

これではコネクションが無駄に増え、リソースの無駄遣いになります。


パターン: Singleton

Singletonパターンは、クラスのインスタンスがひとつしか存在しないことを保証し、そのインスタンスへのグローバルなアクセスポイントを提供します。

+---------------------------+
|      DatabaseConnection   |
+---------------------------+
| - instance: static|null   |
+---------------------------+
| - __construct()           |  ← private にして外からnew不可
| + getInstance(): static   |  ← 唯一の生成ポイント
| + query(sql): array       |
+---------------------------+

PHP 8.x での実装

<?php

final class DatabaseConnection
{
    private static ?self $instance = null;
    private \PDO $pdo;

    // コンストラクタをprivateにして外からのnewを禁止
    private function __construct()
    {
        $this->pdo = new \PDO(
            dsn: 'mysql:host=localhost;dbname=myapp',
            username: 'root',
            password: 'secret',
        );
        $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    }

    // クローンも禁止
    private function __clone() {}

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function query(string $sql, array $params = []): array
    {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}

使う側は getInstance() を呼ぶだけです。

$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();

var_dump($db1 === $db2); // bool(true) — 同一インスタンス

設定クラスへの応用

設定値を一元管理するクラスにも Singleton はよく使われます。

<?php

final class AppConfig
{
    private static ?self $instance = null;
    private array $config = [];

    private function __construct()
    {
        // 設定ファイルを一度だけ読み込む
        $this->config = require __DIR__ . '/../config/app.php';
    }

    private function __clone() {}

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->config[$key] ?? $default;
    }
}

// 使用例
$config = AppConfig::getInstance();
echo $config->get('app_name'); // "MyApp"

Laravelとの対応

LaravelのサービスコンテナはSingletonの概念を洗練させたものです。singleton() メソッドで登録したバインディングは、コンテナから何度取り出しても同じインスタンスが返ります。

// AppServiceProvider.php

public function register(): void
{
    // singleton で登録 → 同一インスタンスを使い回す
    $this->app->singleton(DatabaseConnection::class, function () {
        return new DatabaseConnection(config('database'));
    });

    // bind で登録 → 毎回新しいインスタンスを生成
    $this->app->bind(UserRepository::class, function ($app) {
        return new UserRepository($app->make(DatabaseConnection::class));
    });
}

Laravelではグローバルな app() ヘルパーや App::make() でインスタンスを取得できます。これは静的メソッドによるアクセスより柔軟性があります。


テストでの問題点

Singletonには落とし穴があります。静的なインスタンスはテスト間で状態が引き継がれてしまうのです。

// テストで問題が起きる例
class UserRepositoryTest extends TestCase
{
    public function testFindUser(): void
    {
        $db = DatabaseConnection::getInstance();
        // 前のテストで変更したデータが残っている可能性がある
        $user = $db->query("SELECT * FROM users WHERE id = 1");
        // ...
    }
}

テストごとにインスタンスをリセットする仕組みが必要になりますが、それは Singleton の「ひとつしか存在しない」という前提を崩してしまいます。


代替案: 依存性の注入(DI)

実務では、Singletonを直接使うより依存性の注入(DI)を使うほうが推奨されます。

// DIを使ったアプローチ
class UserRepository
{
    public function __construct(
        private readonly DatabaseConnection $db,
    ) {}

    public function find(int $id): array
    {
        return $this->db->query(
            "SELECT * FROM users WHERE id = ?",
            [$id]
        );
    }
}

// テストではモックを注入できる
class UserRepositoryTest extends TestCase
{
    public function testFindUser(): void
    {
        $mockDb = $this->createMock(DatabaseConnection::class);
        $mockDb->method('query')->willReturn([['id' => 1, 'name' => 'Alice']]);

        $repo = new UserRepository($mockDb); // モックを注入
        $user = $repo->find(1);

        $this->assertEquals('Alice', $user[0]['name']);
    }
}

DIを使えばテストでモック(偽物)を差し込めるため、テストが容易になります。Singletonはシンプルで覚えやすいパターンですが、乱用するとテストしにくいコードになるので注意が必要です。


まとめ

  • Singletonはインスタンスをひとつに制限し、グローバルアクセスポイントを提供する
  • private コンストラクタと static インスタンス変数で実装する
  • DBコネクションや設定クラスなど「一度だけ初期化したいもの」に向いている
  • テストでの状態引き継ぎが問題になるため、LaravelではDI+サービスコンテナを使うのが現代的なアプローチ

次回はFactory Methodパターンで、オブジェクト生成をより柔軟に扱う方法を解説します。