January 19, 2026 (3mo ago) — last updated April 26, 2026 (4d ago)

Singleton в TypeScript: практическое руководство

Практическое руководство по Singleton в TypeScript: реализация, влияние на тестирование и лучшие альтернативы — DI и IoC.

← Back to blog
Cover Image for Singleton в TypeScript: практическое руководство

Шаблон Singleton гарантирует один экземпляр класса и единую точку доступа к общим ресурсам, но он также может создавать скрытые зависимости и осложнять тестирование1. В этом руководстве разберём, когда Singleton оправдан, как безопасно реализовать его в TypeScript и какие современные альтернативы — внедрение зависимостей и контейнеры IoC — стоит предпочесть в новых проектах26.

Освоение шаблона Singleton в TypeScript: полное руководство

Набросок карандашом благородного писца в короне, внимательно изучающего светящийся свиток на столе.

Шаблон Singleton гарантирует, что у класса есть только один экземпляр и единая точка доступа к общим ресурсам, но он также может создавать скрытую связанность и осложнять тестирование1. В этом руководстве разберём, когда Singleton оправдан, как безопасно реализовать его в TypeScript и какие современные альтернативы — внедрение зависимостей и контейнеры IoC — стоит предпочесть в новых проектах26.

Что такое Singleton и зачем он нужен?

Представьте королевство с единственным королевским писцом: он обеспечивает согласованность и порядок. В приложении Singleton служит единственным источником истины для ресурса, который должен быть уникальным.

Ключевые свойства

ХарактеристикаОписание
Единственный экземплярКонструктор скрыт, доступ через статический метод.
Глобальная точка доступаОбычно реализуется как метод getInstance().
Ленивое созданиеЭкземпляр создаётся при первом обращении, что экономит ресурсы.
Централизованное состояниеПодходит для настроек, логирования, управления оборудованием.

Когда Singleton оправдан

Типичные сценарии:

  • Сервис логирования с одной точкой записи.
  • Централизованная конфигурация приложения.
  • Доступ к редкому аппаратному интерфейсу.

Однако простота Singleton часто оборачивается скрытой связанностью и проблемами с тестированием — учитывайте компромиссы заранее3.

Преимущества и недостатки

Весы, сравнивающие структурированные, наблюдаемые паттерны проектирования со сложным, запутанным и экспериментальным кодом.

Преимущества

  • Упрощённый доступ к общему ресурсу.
  • Экономия памяти и ресурсов при ленивой инициализации.
  • Снижение дублирования тяжёлых объектов.

Недостатки

  • Скрытые зависимости, не отражаемые в сигнатурах.
  • Глобальное изменяемое состояние приводит к сложноуловимым багам.
  • Трудности с модульным тестированием и мокированием.

Используйте Singleton осторожно: он даёт простоту доступа, но может усложнить развитие и тестирование проекта.

Влияние на тестирование и связанность

Singleton затрудняют изоляцию тестов, потому что состояние может просачиваться между тестами. Современные команды предпочитают внедрение зависимостей: зависимости становятся явными и легко заменяемыми в тестах37.

Лучшие практики при использовании Singleton

  • Применяйте Singleton только для действительно уникальных ресурсов.
  • Делайте ленивую инициализацию и учитывайте вопросы конкурентности.
  • Объявляйте интерфейсы и обеспечьте возможность замены реализации в тестах.
  • По возможности создавайте экземпляр в корне приложения и передавайте его через конструкторы.

Реализация Singleton в TypeScript

Ниже — типобезопасный пример ConfigManager с приватным конструктором и ленивой инициализацией.

class ConfigManager {
  private static instance: ConfigManager | null = null;
  private settings = new Map<string, any>();

  private constructor() {
    console.log("Инициализация ConfigManager...");
    this.settings.set("API_URL", "https://api.example.com");
    this.settings.set("TIMEOUT", 5000);
  }

  public static getInstance(): ConfigManager {
    if (ConfigManager.instance === null) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }

  public get(key: string): any {
    return this.settings.get(key);
  }
}

// Ошибка: constructor приватный
// const config = new ConfigManager();

Пример применения

class ApiService {
  private apiUrl: string;

  constructor() {
    const config = ConfigManager.getInstance();
    this.apiUrl = config.get("API_URL");
    console.log(`ApiService initialized with API URL: ${this.apiUrl}`);
  }

  public fetchData(): void {
    console.log(`Fetching data from ${this.apiUrl}...`);
  }
}

console.log("Application starting...");
const service1 = new ApiService();
service1.fetchData();
const service2 = new ApiService();
console.log("Application finished.");

При запуске ConfigManager инициализируется только один раз.

Почему у Singleton плохая репутация

Причины критики: скрытая связанность, глобальное изменяемое состояние и усложнённое тестирование. Singletonы могут распространяться по кодовой базе и усложнять рефакторинг, если их не контролировать3.

Конкуренция и состояния гонки

Если Singleton хранит изменяемое состояние, он уязвим к состояниям гонки в многопоточном или асинхронном окружении без синхронизации.

Проблемы тестирования

Мокирование и изоляция тестов усложняются: тестам приходится сбрасывать или переинициализировать глобальные состояния.

Современные альтернативы

Внедрение зависимостей (DI) — предпочтительный подход: зависимости явные и легко заменяемые в тестах. Многие TypeScript-фреймворки поддерживают DI, а библиотеки вроде InversifyJS предлагают гибкие IoC-контейнеры457. Согласно обзорам отрасли, практика DI набирает популярность как способ повышения тестируемости и гибкости кода6.

Пример с DI

interface IConfigManager {
  get(key: string): any;
}

class ApiService {
  private apiUrl: string;

  constructor(config: IConfigManager) {
    this.apiUrl = config.get("API_URL");
  }
}

Теперь ApiService зависит от контракта, и в тестах можно подставлять фейковые реализации.

Как рефакторить Singleton в унаследованной базе

Рефакторьте постепенно:

  1. Найдите все вызовы getInstance().
  2. Определите интерфейс с нужными методами.
  3. Рефакторите потребителей, чтобы они принимали интерфейс через конструктор.
  4. Создавайте и передавайте один экземпляр в корне композиции или используйте DI-контейнер.

Такая стратегия сохраняет поведение, уменьшая скрытую связанность.

Вопросы и ответы — быстрые рекомендации

В: Всегда ли Singleton — плохая идея?

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

В: Как минимизировать проблемы с тестированием?

A: Введите интерфейс, перестройте потребителей на приём зависимости через конструктор и используйте тестовые заглушки; делайте это поэтапно.

В: Как избежать состояния гонки в Singleton?

A: Ограничьте изменяемое состояние, добавьте синхронизацию при доступе или используйте примитивы языка и фреймворка для безопасной инициализации.

Краткие практические выводы

  • Используйте Singleton экономно и только там, где объект по сути уникален.
  • Предпочитайте явное внедрение зависимостей для лучшей тестируемости.
  • Рефакторьте унаследованный код шаг за шагом: интерфейсы → конструкторные зависимости → корневое создание экземпляра.
  • Для новых проектов рассмотрите DI-контейнер вместо статических Singleton — это даёт контроль жизненного цикла без скрытой связанности.

Дополнительные быстрые Q&A (короткие ответы)

Q: Как отличить подходящий случай для Singleton?

A: Если ресурс действительно единственный по природе и имеет ограниченный жизненный цикл, Singleton может быть оправдан.

Q: Можно ли тестировать код с Singleton без рефакторинга?

A: Можно, но неудобно: придётся вручную сбрасывать глобальное состояние или использовать специальные тестовые хелперы.

Q: Как начать миграцию от Singleton к DI в крупном коде?

A: Рефакторите один модуль за раз: вводите интерфейсы и передавайте зависимости через конструкторы, сохраняя текущий Singleton в корне до завершения миграции.


1.
Martin Fowler, “Singleton,” Bliki. https://martinfowler.com/bliki/Singleton.html
2.
3.
Mark Seemann, “Singletons Are Pathological Liars.” https://blog.ploeh.dk/2010/07/28/SingletonsArePathologicalLiar/
4.
NestJS Documentation, “Dependency Injection.” https://docs.nestjs.com/fundamentals/injection
5.
InversifyJS Documentation. https://inversify.io/
6.
State of JS / Stack Overflow — отчёты по использованию и подходам к архитектуре показывают устойчивый рост интереса к TypeScript и практикам DI. https://stateofjs.com/ https://insights.stackoverflow.com/survey/2023
7.
Wikipedia, “Dependency injection.” https://en.wikipedia.org/wiki/Dependency_injection
← Back to blog
🙋🏻‍♂️

ИИ пишет код.
Вы делаете его долговечным.

В эпоху ускорения ИИ чистый код — это не просто хорошая практика — это разница между системами, которые масштабируются, и кодовыми базами, которые рушатся под собственным весом.