← Назад к вопросам
Реализация паттерна 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 контейнеры, которые обеспечивают лучшую гибкость и тестируемость. Если нужен единственный экземпляр - лучше управлять им через контейнер, а не встраивать логику в сам класс.