← Назад к вопросам

Реализация паттерна Singleton

2.0 Middle🔥 191 комментариев
#Архитектура и паттерны#ООП

Условие

Реализовать паттерн Singleton на PHP.

Требования

  • Класс должен гарантировать единственность экземпляра
  • Запретить создание через new
  • Запретить клонирование
  • Запретить десериализацию
  • Потокобезопасность (если применимо)

Пример использования

$instance1 = Database::getInstance();
$instance2 = Database::getInstance();

// $instance1 === $instance2 должно быть true

Дополнительно

  • Объясните плюсы и минусы паттерна
  • Когда стоит использовать Singleton
  • Альтернативы (DI контейнер)
  • Проблемы при тестировании

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

1. Простая реализация Singleton

<?php

class Database {
    // Статическое свойство для хранения единственного экземпляра
    private static ?self $instance = null;

    // Приватный конструктор - запрещает создание через new
    private function __construct() {}

    // Приватный метод клонирования - запрещает клонирование
    private function __clone() {}

    // Приватный метод десериализации - запрещает десериализацию
    public function __wakeup() {
        throw new Exception("Нельзя десериализовать Singleton");
    }

    // Статический метод для получения единственного экземпляра
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function connect() {
        return "Connected to database";
    }
}

// Использование
$db1 = Database::getInstance();
$db2 = Database::getInstance();

echo ($db1 === $db2) ? "One instance" : "Different instances"; // Output: One instance

2. Потокобезопасная реализация (Double-Checked Locking)

<?php

class ThreadSafeDatabase {
    private static ?self $instance = null;
    private static object $lock;

    private function __construct() {}
    private function __clone() {}
    public function __wakeup() {
        throw new Exception("Cannot unserialize singleton");
    }

    public static function getInstance(): self {
        // Первая проверка (без блокировки)
        if (self::$instance === null) {
            // Вторая проверка (с блокировкой) - гарантирует потокобезопасность
            if (self::$instance === null) {
                self::$instance = new self();
            }
        }
        return self::$instance;
    }
}

3. Реализация с параметрами (более гибкий вариант)

<?php

class ConfiguredSingleton {
    private static array $instances = [];
    private array $config = [];

    private function __construct(array $config = []) {
        $this->config = $config;
    }

    private function __clone() {}
    public function __wakeup() {
        throw new Exception("Cannot unserialize singleton");
    }

    // Named instances - разные "Singleton" с разными конфигами
    public static function getInstance(string $name = "default", array $config = []): self {
        if (!isset(self::$instances[$name])) {
            self::$instances[$name] = new self($config);
        }
        return self::$instances[$name];
    }

    public function getConfig(): array {
        return $this->config;
    }
}

// Использование
$config1 = ConfiguredSingleton::getInstance("dev", ["debug" => true]);
$config2 = ConfiguredSingleton::getInstance("prod", ["debug" => false]);
echo $config1 === ConfiguredSingleton::getInstance("dev") ? "Same" : "Different"; // Same

4. Реализация через trait (переиспользуемый Singleton)

<?php

trait SingletonTrait {
    private static array $instances = [];

    private function __clone() {}
    public function __wakeup() {
        throw new Exception("Cannot unserialize singleton");
    }

    public static function getInstance(): self {
        $class = static::class;
        if (!isset(self::$instances[$class])) {
            self::$instances[$class] = new static();
        }
        return self::$instances[$class];
    }
}

class Logger {
    use SingletonTrait;

    private function __construct() {}
    private array $logs = [];

    public function log(string $message): void {
        $this->logs[] = $message;
    }

    public function getLogs(): array {
        return $this->logs;
    }
}

// Использование
Logger::getInstance()->log("Event 1");
Logger::getInstance()->log("Event 2");
echo count(Logger::getInstance()->getLogs()); // 2

5. Проблемы при тестировании

<?php

// ПРОБЛЕМА 1: Состояние между тестами

class DatabaseTest extends TestCase {
    public function test_first(): void {
        $db = Database::getInstance();
        $db->setUser("admin");
        $this->assertEquals("admin", $db->getUser());
    }

    public function test_second(): void {
        // ПРОБЛЕМА: Экземпляр из предыдущего теста остается!
        $db = Database::getInstance();
        // $db->getUser() все еще возвращает "admin"
    }
}

// РЕШЕНИЕ 1: Добавить метод для сброса (не рекомендуется для production)

class TestableDatabase {
    private static ?self $instance = null;

    private function __construct() {}
    private function __clone() {}

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

    // Только для тестирования!
    public static function resetInstance(): void {
        self::$instance = null;
    }
}

class ImprovedDatabaseTest extends TestCase {
    protected function tearDown(): void {
        parent::tearDown();
        TestableDatabase::resetInstance(); // Сброс после каждого теста
    }

    public function test_isolation(): void {
        $db = TestableDatabase::getInstance();
        // Теперь тест не зависит от предыдущих
    }
}

// РЕШЕНИЕ 2: Использовать DI контейнер вместо Singleton (РЕКОМЕНДУЕТСЯ)

interface DatabaseInterface {
    public function connect(): string;
}

class RealDatabase implements DatabaseInterface {
    public function connect(): string {
        return "Connected to real database";
    }
}

class MockDatabase implements DatabaseInterface {
    public function connect(): string {
        return "Mock connection";
    }
}

class ApplicationWithDI {
    private DatabaseInterface $db;

    public function __construct(DatabaseInterface $db) {
        $this->db = $db; // Инъекция зависимости
    }

    public function run(): string {
        return $this->db->connect();
    }
}

// Тест с DI - намного проще!
class ApplicationTest extends TestCase {
    public function test_with_mock(): void {
        $mockDb = new MockDatabase();
        $app = new ApplicationWithDI($mockDb);
        $this->assertEquals("Mock connection", $app->run());
    }
}

6. Сравнение подходов

Плюсы Singleton:

  • Единственный экземпляр в памяти
  • Глобальная точка доступа
  • Ленивая инициализация
  • Простота использования

Минусы Singleton:

  • Скрытая зависимость (сложно отследить)
  • Сложное тестирование
  • Глобальное состояние (усложняет отладку)
  • Нарушает SOLID принципы
  • Конкурентность в многопоточных приложениях
  • Сложное рефакторить после внедрения

Когда использовать Singleton:

  • Логирование (Logger)
  • Конфигурация приложения
  • Кеш
  • Пул соединений с БД
  • Service Locator (хотя это анти-паттерн)

Альтернативы:

<?php

// 1. DI контейнер (ЛУЧШИЙ ВЫБОР)
class Container {
    private array $services = [];

    public function register(string $name, callable $factory): void {
        $this->services[$name] = $factory;
    }

    public function get(string $name) {
        if (!isset($this->services[$name])) {
            throw new Exception("Service not found: $name");
        }
        return $this->services[$name]($this);
    }
}

$container = new Container();
$container->register("database", fn() => new Database());
$db = $container->get("database");

// 2. Статический класс (если состояние не нужно)
class Logger {
    private static array $logs = [];

    public static function log(string $message): void {
        self::$logs[] = $message;
    }
}

Logger::log("Event");

// 3. Фасад (Laravel подход)
Facade::shouldReceive("log")->with("Event");

7. Тест Singleton

<?php

use PHPUnit\Framework\TestCase;

class SingletonTest extends TestCase {
    public function test_singleton_instance(): void {
        $instance1 = Database::getInstance();
        $instance2 = Database::getInstance();
        $this->assertSame($instance1, $instance2);
    }

    public function test_cannot_instantiate(): void {
        $this->expectException(Exception::class);
        new Database();
    }

    public function test_cannot_clone(): void {
        $this->expectException(Exception::class);
        $db = Database::getInstance();
        clone $db;
    }

    public function test_cannot_unserialize(): void {
        $this->expectException(Exception::class);
        $db = Database::getInstance();
        unserialize(serialize($db));
    }
}

8. Рекомендация

Вывод: Singleton полезен в конкретных ситуациях, но не стоит переусложнять архитектуру его использованием. Современные фреймворки (Laravel, Symfony) используют DI контейнеры, которые обеспечивают лучшую гибкость и тестируемость. Если нужен единственный экземпляр - лучше управлять им через контейнер, а не встраивать логику в сам класс.