Одиночка

Одиночка — это порождающий паттерн проектирования, который гарантирует, что у класса будет только один экземпляр, и предоставляет к этому экземпляру глобальную точку доступа.
Проблема
В статье про Прототип двигатели измерялись через DynoService, который обращается к внешнему стенду. У такого стенда есть физическое ограничение: он один, и держать к нему больше одного открытого соединения нельзя — параллельные подключения будут мешать друг другу и портить показания.
Наивный вариант — создавать соединение там, где оно нужно:
class DynoService
{
public static function measure(string $model): int
{
$connection = new DynoStandConnection(); // новое соединение при каждом вызове
return $connection->measure($model);
}
}Каждый вызов открывает новое соединение к стенду, хотя нужно ровно одно на всё приложение. Хранить соединение в глобальной переменной — не лучше: глобальные переменные в PHP ничем не защищены от случайной перезаписи и не контролируют момент создания объекта.
Решение
Одиночка запрещает создавать объект через new напрямую и вместо этого предоставляет единственную точку доступа — статический метод, который либо создаёт экземпляр при первом обращении, либо возвращает уже существующий:
final class DynoStandConnection
{
private static ?self $instance = null;
private function __construct()
{
// Дорогая операция: установка соединения с физическим стендом.
}
public static function getInstance(): self
{
return self::$instance ??= new self();
}
public function measure(string $model): int
{
// ...отправка запроса на стенд и чтение показаний.
}
}Ключевые детали:
- Приватный конструктор — снаружи класса вызвать
new DynoStandConnection()невозможно, единственный способ получить объект —getInstance(). final— если класс можно унаследовать, наследник может создать свой собственный экземпляр в обход приватного конструктора родителя.self::$instance ??= new self()— экземпляр создаётся лениво, при первом реальном обращении, а не при загрузке класса.
Теперь DynoService работает с единственным на всё приложение соединением:
class DynoService
{
public static function measure(string $model): int
{
return DynoStandConnection::getInstance()->measure($model);
}
}Защита от обхода через клонирование и сериализацию
В статье про Прототип clone создавал независимую копию объекта — для Одиночки это ровно та лазейка, которую нужно закрыть, иначе clone DynoStandConnection::getInstance() даст второй экземпляр в обход всей защиты:
final class DynoStandConnection
{
private static ?self $instance = null;
private function __construct() {}
public static function getInstance(): self
{
return self::$instance ??= new self();
}
private function __clone(): void
{
// Приватный __clone запрещает клонирование снаружи класса.
}
public function __wakeup(): void
{
throw new \LogicException('Cannot unserialize a singleton.');
}
public function measure(string $model): int
{
// ...
}
}__wakeup() закрывает второй обходной путь — десериализацию через unserialize(), которая тоже создаёт объект в обход конструктора.
Из чего состоит паттерн
- Одиночка (
DynoStandConnection) — единственный класс паттерна: сам хранит свой единственный экземпляр в статическом поле, сам управляет его созданием через статический метод доступа и сам защищает себя от обхода этой защиты.
В отличие от остальных порождающих паттернов, здесь нет отдельных ролей продукта, создателя или строителя — класс полностью самодостаточен.
Одиночка — самый спорный паттерн
В отличие от Фабричного метода, Абстрактной фабрики, Строителя и Прототипа, Одиночку часто называют антипаттерном, и на то есть причины:
Скрытая зависимость. Сигнатура DynoService::measure() не показывает, что метод зависит от соединения к стенду — это видно только заглянув внутрь реализации. Обычная зависимость, переданная через конструктор, видна сразу.
Глобальное изменяемое состояние. Одиночка — это, по сути, глобальная переменная с процедурой доступа. Любой код в приложении может незаметно повлиять на его состояние.
Проблемы с тестированием. Тест, использующий DynoService, не может подменить DynoStandConnection на заглушку — метод жёстко обращается к конкретному классу через getInstance(). Состояние одиночки к тому же переживает между тестами, если его не сбрасывать вручную.
Более тестируемая альтернатива — обычный объект, время жизни которого контролирует не сам класс, а внешняя точка сборки приложения (композиционный корень) или DI-контейнер:
class CarDiagnostics
{
public function __construct(
private readonly DynoStandConnection $connection,
) {}
public function measure(string $model): int
{
return $this->connection->measure($model);
}
}
// Единственный экземпляр создаётся один раз в точке сборки приложения...
$connection = new DynoStandConnection();
// ...и передаётся явно всем, кому он нужен.
$diagnostics = new CarDiagnostics($connection);Единственность экземпляра здесь обеспечена не классом DynoStandConnection (который теперь ничем не отличается от обычного класса с публичным конструктором), а дисциплиной сборки: соединение создаётся один раз и передаётся всем зависимым объектам явно. Зависимость видна в конструкторе, а в тестах CarDiagnostics можно передать любую подставную реализацию.
Когда применять
- Ограничение на единственность продиктовано реальным миром, а не удобством — как физическое соединение к единственному диагностическому стенду, а не «просто чтобы было удобно достучаться откуда угодно».
- Небольшой скрипт или CLI-утилита, где нет инфраструктуры для явной передачи зависимостей и накладные расходы DI-контейнера не оправданы.
Плюсы и минусы
Одиночка гарантирует единственность экземпляра на уровне языка, а не соглашения, даёт ленивую инициализацию и глобальную точку доступа без глобальных переменных как таковых.
Минусы перевешивают эти плюсы чаще, чем кажется на первый взгляд: скрытые зависимости, глобальное состояние, сложности с тестированием и жёсткая связанность с конкретным классом. В большинстве случаев то же самое единственное соединение можно получить через DI-контейнер или явную передачу зависимости — с той же гарантией единственности, но без побочных эффектов Одиночки.
Итог
На этом можно закрыть основные порождающие паттерны GoF: Фабричный метод, Абстрактная фабрика, Строитель, Прототип и Одиночка. Первые четыре решают вопрос «как создать объект, не привязываясь к конкретному классу или процессу создания». Одиночка — единственный из пяти, который вместо этого решает вопрос «как гарантировать, что объект будет ровно один», и именно поэтому его стоит применять только тогда, когда единственность — часть предметной области, а не способ спрятать глобальную переменную.