handbookНаписать

patterns/creational/singleton/README.md · тема · 4 языка · 15 тестов

+ практика: тарифы, загруженные один раз

Одиночка (Singleton) ​

Гарантирует один экземпляр на процесс и даёт к нему глобальную точку доступа. Первое полезно, второе почти всегда вредно.

Проблема ​

Настройки приложения читаются один раз и нужны везде: в приветствии, в ценах, в логах. Передавать объект настроек через десять конструкторов кажется утомительным, а создавать его заново в каждом месте — дорого и опасно: копии разойдутся.

Решение ​

Класс сам следит, чтобы экземпляр был один: конструктор закрыт, статический метод instance() создаёт объект при первом обращении и дальше возвращает его же. Клиент Greeter достаёт настройки через эту точку доступа, не получая их снаружи.

Так приём описан в книге. На практике важно разделить две его половины. «Один экземпляр на процесс» — нормальное требование, и его обеспечивают модуль, пакет или точка сборки приложения. «Глобальная точка доступа» — скрытая зависимость: по сигнатуре Greeter не видно, что он зависит от настроек, а тесты делят состояние между собой. Поэтому тесты ниже вынуждены выставлять значение явно перед каждой проверкой.

Структура ​

instance()

AppConfig

-instance: AppConfig

-values: Map

-AppConfig()

+instance() : AppConfig

+set(key, value)

+get(key, default) : string

Greeter

+greeting(name) : string

Одиночка держит и правило единственности, и состояние, и точку доступа в одном классе. Это удобно писать и неудобно заменять: подменить AppConfig на тестовый нельзя, не трогая сам класс.

Как это собирается ​

Одиночку наполняет загрузка приложения: один раз на процесс она читает окружение и кладёт lang в AppConfig::instance(). Дальше Greeter и всё остальное читают настройки через ту же точку доступа, ничего не получая в конструкторе. В примере обработчик запроса, как это часто делает middleware, переключает язык для пользователя, который просит русский, и записывает его прямо в общий объект.

Вывод показывает цену: Ann получила Привет, но и следующий запрос Bob без предпочтений тоже получил Привет, потому что состояние одно на процесс. Вызывающий код вынужден сам возвращать значение после запроса (finally, в Go defer), и только тогда Bob снова видит Hello. Честное решение - не писать в одиночку на запрос, а передавать язык параметром или держать его в объекте запроса.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

Клиент во всех языках одинаково короток и одинаково непрозрачен: зависимость от настроек видна только внутри метода. В Go и Python сам язык уже даёт «один экземпляр на процесс» — пакет и модуль, поэтому классический класс там не пишут.

ЯзыкОсобенность
PHPКлассика: приватный конструктор, статическое поле, ??=; __clone закрыт
GoПеременная пакета плюс sync.Once: создание один раз и потокобезопасно; пакет и есть точка доступа
PythonМодуль загружается один раз; functools.cache на фабрике делает ленивый одиночка одной строкой
TypeScriptКлассическая статическая форма ради паттерна; в проекте ES-модуль с export const уже одиночка

Запуск: make test T=patterns/creational/singleton

Плюсы и минусы ​

Тесты показывают приём и его цену одновременно: два вызова дают один объект, состояние видно отовсюду, клиент реагирует на настройки. Но каждый тест обязан выставить значение сам, потому что предыдущий мог его оставить, а в Go отдельный тест проверяет, что гонка первых вызовов не создаёт два экземпляра.

  • ✅ Ровно один экземпляр, создаётся лениво.
  • ✅ Доступ из любого места без проводки зависимостей.
  • ❌ Скрытая зависимость: по сигнатуре клиента не видно, от чего он зависит.
  • ❌ Общее состояние между тестами и потоками; подменить экземпляр нельзя.
  • ❌ Нарушает единственную ответственность: класс и делает дело, и управляет своим временем жизни.

Когда не применять ​

  • Почти всегда в прикладном коде: «один экземпляр» обеспечивает точка сборки приложения или DI-контейнер, а зависимость передаётся явно. См. практику.
  • Когда объект нужен в тестах в разных конфигурациях.
  • Когда «один на процесс» на самом деле «один на запрос» или «один на пользователя».

Отличия между языками ​

  • PHP — классическая форма работает, но в PHP-FPM «процесс» живёт один запрос, и одиночка часто ничего не экономит. Фреймворки держат один экземпляр в контейнере, а классы его получают через конструктор.
  • Go — sync.Once решает потокобезопасность, которой в учебном коде обычно нет. Но пакетная переменная — та же глобальная точка доступа со всеми минусами; в Go её принято заменять явной передачей.
  • Python — модуль импортируется один раз, объект на уровне модуля и есть одиночка. functools.cache даёт ленивость; для тестов есть app_config.cache_clear(), и это уже признак проблемы.
  • TypeScript — модули кэшируются, export const config = new AppConfig() достаточно. Статическая форма нужна только для ленивого создания или когда класс должен запрещать new.

Практика ​

Учебный одиночка смешивает два требования: один экземпляр и глобальный доступ. В проекте нужно только первое, а второе приносит тесты, зависящие от порядка, и гонки при первом обращении. Практика: тарифы, загруженные один раз оставляет единственность, но убирает точку доступа: ресурс загружается провайдером лениво и потокобезопасно, клиенты получают его через конструктор, а решение «один на процесс» живёт в точке сборки приложения.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS