January 19, 2026 (3mo ago)

Опанування патерну Singleton: Повний посібник для розробника

Дізнайтеся про патерн Singleton: коли його використовувати, практичні приклади на TypeScript та важливі альтернативи.

← Back to blog
Cover Image for Опанування патерну Singleton: Повний посібник для розробника

Дізнайтеся про патерн Singleton: коли його використовувати, практичні приклади на TypeScript та важливі альтернативи.

Опанування патерну Singleton у TypeScript: Повний посібник

Ескіз олівцем королівського писаря в короні, який старанно вивчає світлий сувій на столі.

У світі розробки програмного забезпечення деякі інструменти потужні, але їх слід використовувати обережно. Патерн Singleton — один із таких. У своїй основі це проста ідея: гарантувати, що клас має лише один екземпляр, і надати єдиний, глобальний спосіб доступу до нього.1

Уявіть його як центрального менеджера конфігурації або виділений сервіс логування для всього вашого додатка. Ви б не хотіли, щоб існувало кілька суперечливих об'єктів конфігурації, так само як не бажаєте, щоб записи журналу розкидані по різних файлах через конкуруючі екземпляри логера. Singleton наводить порядок, запобігаючи дублюванню, економить пам'ять і уникає хаосу. Це базовий патерн для керування доступом до спільних ресурсів.

Що таке патерн Singleton і коли він корисний?

Уявіть середньовічне королівство з лише одним офіційним Королівським Писарем. Ця особа є єдиною уповноваженою фіксувати королівські декрети. Це забезпечує послідовність усіх законів і оголошень, їх належну авторизацію та зберігання в єдиному, визначальному реєстрі. Якби будь-хто міг просто вирішити стати писарем, королівство швидко занурилося б у хаос із суперечливими записами й масовою плутаниною.

Патерн Singleton у програмному забезпеченні діє за тим самим принципом. Його основне завдання — обмежити клас так, щоб з нього можна було створити лише один об'єкт. Цей єдиний екземпляр стає джерелом істини для конкретного завдання й доступний звідусіль у кодовій базі. Так ви контролюєте ресурси, які ніколи не повинні дублюватися.

Основна мета та аналогія

Singleton — це не просто про те, щоб заборонити створювати нові об'єкти; це про централізацію контролю. Так само як Королівський Писар забезпечує єдину точку доступу до офіційних записів королівства, екземпляр Singleton пропонує загальнодоступний шлюз до спільного ресурсу. Він перешкоджає різним частинам додатка створювати власні ізольовані та потенційно суперечливі версії.

Типовим прикладом є пул з'єднань з базою даних. Вам явно не варто, щоб кожен компонент застосунку відкривав власне окреме підключення до бази — це гарантовано швидко виснажить ресурси сервера і призведе до падіння продуктивності. Натомість Singleton може керувати одним пулом з'єднань, ефективно розподіляючи їх за потреби.

Основна ідея проста, але потужна: один клас, один екземпляр, одна глобальна точка доступу. Ця структура гарантує, що всі взаємодії з певним ресурсом проходять через єдиний контрольований канал.

Патерн Singleton у загальному огляді

ХарактеристикаОпис та обґрунтування
Єдиний екземплярКлас спроектовано так, щоб мати лише один екземпляр протягом життєвого циклу додатка, часто це забезпечується приватним конструктором.
Глобальна точка доступуСтатичний метод (наприклад, getInstance()) надає один відомий спосіб доступу до екземпляра звідусіль у коді.
Лінива ініціалізаціяЄдиний екземпляр часто створюється вперше під час першого запиту, а не під час запуску додатка, що може покращити продуктивність.
Керування станомВін виступає централізованим місцем для певної частини глобального стану, наприклад налаштувань додатка чи сесії користувача.

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

Практичні випадки використання

Хоча патерн Singleton має своїх критиків, він не позбавлений легітимних випадків застосування. Він найбільш ефективний, коли у вас є ресурс, який за своєю природою унікальний у системі.

Ось кілька сценаріїв, де використання Singleton має сенс:

  • Сервіси логування: один екземпляр логера гарантує, що всі події надходять у той самий файл або потік.
  • Менеджмент конфігурації: єдине джерело налаштувань додатка уникає невідповідностей між модулями.
  • Доступ до апаратного інтерфейсу: один інтерфейс до пристрою запобігає конфліктним командам.

Переваги та недоліки використання Singleton

Ваги, що порівнюють структуровані, спостережувані патерни дизайну з складним, заплутаним і експериментальним кодом.

Патерн Singleton може здаватися надійним інструментом, коли потрібна єдина точка доступу до спільного ресурсу. Він дає простий спосіб керувати такими речами, як об'єкт конфігурації або сервіс логування у всьому додатку.

Переваги Singleton

  • Глобальна точка доступу спрощує використання між модулями.
  • Економія ресурсів завдяки лінивій ініціалізації може знизити витрати на запуск.
  • Зменшення дублювання запобігає створенню кількох суперечливих екземплярів дорогих ресурсів.

Недоліки Singleton

  • Тісне зв'язування: класи можуть приховувати залежності, звертаючись до глобального стану.
  • Глобальний стан: спільний змінний стан може спричинити важко виявлювані баги.
  • Приховані побічні ефекти: методи, що залежать від Singleton, не відображають цю залежність у своїх сигнатурах, що ускладнює розуміння і тестування.

Вплив на тестування та зв'язність

Singleton ускладнює модульне тестування, оскільки він вводить глобальний, персистентний стан. Тести ризикують «текти» станом між прогоном, і підміна (mocking) Singleton може стати незручною. Сучасні команди часто віддають перевагу впровадженню залежностей (dependency injection), бо воно робить залежності явними та легкими для заміни під час тестів.3

Балансування компромісів

Коли вирішуєте, чи використовувати Singleton, зважте зручність проти довгострокової підтримуваності та тестованості. Для спадкових кодових баз інкрементальні рефактори в бік впровадження залежностей часто є найбезпечнішим шляхом: зберігайте поведінку, зменшуючи приховану зв'язаність і покращуючи тестування.

Singleton обмінює простоту на глобальний стан, тож обирайте обережно, виходячи з потреб вашої команди.

Основні висновки

  • Використовуйте Singleton помірно і тільки для сервісів, які дійсно мають бути унікальними.
  • Віддавайте перевагу явному впровадженню залежностей для кращої декуплінгу і тестованості.
  • Якщо ви змушені використовувати Singleton, реалізуйте його ліниво і будьте уважні до проблем конкурентності та безпеки потоків.
  • Для спадкових систем поступово виводьте Singleton, вводячи інтерфейси та DI у корені композиції.

Як реалізувати патерн Singleton у TypeScript

Діаграма, що показує ConfigManager із замком, демонструючи патерн Singleton за допомогою getInstance у TypeScript.

Перейдемо від абстракції до практики і побудуємо сучасний, типобезпечний Singleton у TypeScript. Секретна комбінація — private конструктор та static метод, що виступає привратником. Це поєднання гарантує, що жодна інша частина вашого застосунку не зможе створити новий екземпляр; усі проходять через одну точку входу.2

Для нашого практичного прикладу ми створимо ConfigManager. Цей клас завантажує і надає налаштування додатка, гарантує, що всі компоненти читають з одного джерела істини.

Побудова типобезпечного ConfigManager

// Практичний приклад патерну Singleton для керування конфігурацією.

class ConfigManager {
  // 1. Приватна статична властивість для зберігання єдиного екземпляра.
  private static instance: ConfigManager;

  // 2. Місце для збереження наших даних конфігурації.
  private settings: Map<string, any> = new Map();

  // 3. Приватний конструктор. Це забороняє виклик `new ConfigManager()` з інших місць.
  private constructor() {
    // У реальному додатку ви б завантажували з файлу, змінних оточення або сервісу.
    console.log("Ініціалізація екземпляра ConfigManager...");
    this.settings.set("API_URL", "https://api.example.com");
    this.settings.set("TIMEOUT", 5000);
  }

  // 4. Публічний статичний метод, який контролює доступ до єдиного екземпляра.
  public static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }

  // 5. Звичайний публічний метод для отримання конкретної налаштування.
  public get(key: string): any {
    return this.settings.get(key);
  }
}

// TypeScript не дозволить вам цього зробити:
// const config = new ConfigManager(); // Помилка: Конструктор класу 'ConfigManager' є приватним.

Ця структура використовує модифікатори доступу TypeScript, щоб реалізувати клас з одним екземпляром і лінивою ініціалізацією. Приватний конструктор і патерн зі статичним екземпляром прості й ефективні для багатьох простих потреб.

Використання Singleton у сервісі

class ApiService {
  private apiUrl: string;

  constructor() {
    const config = ConfigManager.getInstance();
    this.apiUrl = config.get("API_URL");
    console.log(`ApiService ініціалізовано з API URL: ${this.apiUrl}`);
  }

  public fetchData(): void {
    console.log(`Отримання даних з ${this.apiUrl}...`);
    // Тут мав би бути реальний логіку отримання даних.
  }
}

// --- Точка входу додатка ---
console.log("Запуск додатка...");

const service1 = new ApiService();
service1.fetchData();

const service2 = new ApiService();
console.log("Додаток завершив роботу.");

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

Чому Singleton має таку погану репутацію

Патерн Singleton приваблює простотою та наданням глобально доступного об'єкта. Проблеми виникають через приховану зв'язаність, глобальний змінний стан і кошмари з тестування. Коли клас невимушено звертається до глобального екземпляра, він приховує залежність, яка мала б бути явною в його конструкторі. Це ускладнює розуміння системи і її тестування.3

Кошмари конкурентності та глобального стану

Singleton з станом може спричиняти умови гонки у конкурентних сценаріях. Розгляньте SessionCounter, де два одночасні запити інкрементують той самий лічильник. Без синхронізації обидва можуть прочитати однакове початкове значення і записати конфліктні оновлення. Ці баги залежать від часу і важко відтворюються.

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

Singleton ускладнює модульні тести, тому що стан може просочуватися між тестами, і підміна екземпляра стає важкою. Тести можуть почати залежати від порядку виконання, і набір тестів стає нестабільним. Ось чому команди часто переходять на впровадження залежностей: воно робить залежності явними й легкими для підміни.

Попри ці проблеми, Singleton-и досі широко використовуються в кодових базах. Вони часто розповзаються по проєкту після первинного введення, тому аудит і поступовий рефакторинг важливі під час покращення архітектури.

Сучасні альтернативи патерну Singleton

Побачивши ризики, які несе Singleton, ви, ймовірно, запитаєте: «Що ж використовувати натомість?» Впровадження залежностей (Dependency Injection, DI) — це найпоширеніший підхід для керування спільними ресурсами. DI робить залежності явними і покращує тестованість та модульність.

Впровадження залежностей проти Singleton

Порівняйте початковий ApiService, який звертається до Singleton, з версією, що приймає менеджера конфігурації через свій конструктор.

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

class ApiService {
  private apiUrl: string;

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

Тепер ApiService залежить лише від контракту IConfigManager. Під час тестів ви можете передати фейковий або мок-об'єкт, що робить тести швидкими та передбачуваними.

Інвертуючи контроль над створенням залежностей, компоненти стають більш сфокусованими та гнучкими. Ця ідея є суттю принципу інверсії залежностей (Dependency Inversion Principle).

Роль контейнерів IoC

Контейнер інверсії управління (IoC) керує створенням об'єктів і їх ін'єкцією у вашому застосунку. Популярні TypeScript-фреймворки надають вбудовані DI-контейнери, такі як NestJS і Angular, або бібліотеки як InversifyJS для загальних проєктів.45

Контейнери дозволяють обирати, як об'єкти мають ділитися: тимчасово (transient), у межах сесії (scoped) або з життєвим циклом, подібним до singleton. Це дає переваги одного спільного екземпляра без прихованої зв'язаності програматичного Singleton.

Як рефакторити Singleton у спадковому коді

Працюйте поступово. Визначте, де використовується Singleton, опишіть чіткий інтерфейс, що описує його поведінку, і почніть змінювати окремих споживачів, щоб вони приймали інтерфейс через ін'єкцію в конструктор. Потім підключіть конкретний екземпляр в корені композиції або дозвольте DI-контейнеру керувати ним.

Крок 1: Виявити й ізолювати Singleton

Знайдіть кожен виклик MySingleton.getInstance() і окресліть межі відповідальностей Singleton. Визначте інтерфейс, що перераховує публічні методи, які вам потрібні.

Крок 2: Впровадити DI поступово

Рефакторьте одного споживача за раз:

  1. Змініть конструктор, щоб він приймав інтерфейс.
  2. Замініть прямі виклики getInstance() викликами до інжектованого екземпляра.
  3. В точці створення передавайте екземпляр Singleton до завершення міграції.

Це зберігає стабільність додатка, поки ви зменшуєте приховану зв'язаність.

Крок 3: Замінити Singleton на керований екземпляр

Коли споживачі прийматимуть залежності через інтерфейс, ви зможете видалити статичний getInstance() і зробити реалізацію звичайним класом з публічним конструктором. Створіть один екземпляр у корені композиції й передавайте його за потреби, або дозвольте DI-контейнеру керувати життєвим циклом і областю видимості.

Відповіді на ваші гарячі питання про Singleton

Чи завжди Singleton — погана ідея?

Не завжди. Він має сенс для дійсно унікальних, безстанних сервісів, як центральний логер або апаратний адаптер. Навіть у таких випадках DI і контрольований корінь композиції часто пропонують ту саму поведінку з кращою тестованістю.

Як Singleton псує модульне тестування?

Він вводить глобальний, персистентний стан, що може просочуватися між тестами і ускладнює підміну (mocking). Через це тести можуть стати залежними від порядку виконання й нестабільними. DI спрощує тести, оскільки мок-об'єкти можна інжектувати безпосередньо.

Хіба статичний клас — це по суті те саме?

Ні. Статичний клас містить лише статичні члени і не може бути інстанційований. Singleton має реальний екземпляр і може реалізовувати інтерфейси та передаватися як об'єкт. Обидва підходи можуть призвести до тісної зв'язаності, тому для гнучкості краще використовувати DI.

Наступні кроки для вашої команди

Почніть розмову про те, де, якщо взагалі, дійсно потрібен один спільний екземпляр. Прототипуйте DI в ізольованому модулі, прийміть чіткі стандарти кодування й використовуйте інструменти для вимірювання технічного боргу. Парне програмування і безперервний зворотний зв'язок допомагають робити рефактори безпечно і ефективно.

Пам'ятайте: жоден патерн проектування не є панацеєю. Singleton має своє місце, але його слід застосовувати обачно й у поєднанні з чистими інтерфейсами й чіткими правилами власності.

FAQ — Короткі питання й відповіді

Q: Коли доречно використовувати Singleton? A: Коли ресурс дійсно унікальний і безстанний, наприклад централізований логер або апаратний адаптер. Там, де можливо, віддавайте перевагу DI.

Q: Як тестувати код, що зараз використовує Singleton? A: Введіть інтерфейс, рефакторьте споживачів, щоб вони приймали інтерфейс, і інжектуйте тестову підстановку. Робіть це поступово, щоб уникнути масштабних регресій.

Q: Який найнадійніший шлях видалити Singleton зі спадкового додатка? A: Відобразіть випадки використання, визначте інтерфейси, рефакторьте споживачів для прийому залежностей, потім створіть і інжектуйте один екземпляр у корені композиції або використайте DI-контейнер.


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/
← Back to blog
🙋🏻‍♂️

ШІ пише код.
Ви робите його довговічним.

В епоху прискорення ШІ чистий код — це не просто хороша практика — це різниця між системами, які масштабуються, та кодовими базами, які руйнуються під власною вагою.