Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Патерн синглтон». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Патерн синглтон (Singleton)** - це креаційний патерн, який гарантує, що клас має рівно один екземпляр з глобальною точкою доступу. ```typescript class Database { private static instance: Database; private constructor() {} static getInstance(): Database { if (!Database.instance) { Database.instance = new Database(); } return Database.instance; } } const db1 = Database.getInstance(); const db2 = Database.getInstance(); console.log(db1 === db2); // true - один об'єкт ``` **Головне:** приватний конструктор блокує `new`; `getInstance()` щоразу повертає один і той самий об'єкт. Використовуй для логерів, конфігів або пулів з'єднань.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Патерн синглтон (Singleton)** - це креаційний патерн проектування, який обмежує клас єдиним екземпляром і надає глобальну точку доступу до нього. ## Теорія ### Коротко - Аналогія: Білий дім - одна будівля, один президент, усі звертаються через одну адресу. - Головна різниця: звичайний клас дозволяє викликати `new` скільки завгодно; синглтон блокує кожен `new` після першого. - Використовуй, коли один об'єкт має контролювати спільний ресурс (логер, пул DB, конфіг). Для утиліт без стану не потрібен. - У Node.js clusters кожен процес отримує власний екземпляр синглтону. Між процесами він НЕ ділиться. ### Швидкий приклад ```typescript class Singleton { private static instance: Singleton; private constructor() { // private: ніхто не може написати new Singleton() ззовні } static getInstance(): Singleton { if (!Singleton.instance) { Singleton.instance = new Singleton(); // створюється один раз } return Singleton.instance; } } const a = Singleton.getInstance(); const b = Singleton.getInstance(); console.log(a === b); // true - один і той самий об'єкт ``` Два виклики, один об'єкт. Ось і весь механізм. ### Ключова різниця Звичайний клас дозволяє викликати `new` скільки завгодно і щоразу отримувати новий об'єкт. Синглтон робить конструктор приватним і направляє весь доступ через статичний метод `getInstance()`. Перший виклик створює об'єкт і зберігає посилання в статичному полі. Кожен наступний виклик повертає те саме посилання. Без нових алокацій, без дублікатів. ### Коли використовувати - **Логер** - один потік логів для всього застосунку, а не по одному на модуль. - **Пул з'єднань з БД** - розподіляємо обмежені з'єднання по всій кодовій базі. - **Конфіг застосунку** - завантажуємо налаштування один раз при старті, читаємо будь-де. - **Кеш** - одне сховище в пам'яті, консистентний стан між фічами. - **Не використовуй**, коли клас не тримає спільний стан. Утиліта з чистими функціями не потребує синглтону. Якщо потрібно мокати в тестах, розглянь dependency injection. ### Як це працює під капотом V8 (Node.js/Chrome) зберігає приватне статичне поле `#instance` як прихований атрибут класу, недоступний ззовні. Кожен виклик `getInstance()` робить швидкий пошук по статичному полю. Якщо поле `null` - алокує один раз через `new` і зберігає посилання. Наступні виклики пропускають алокацію повністю. Одна річ, яка часто дивує: у Node.js з worker threads або в режимі cluster кожен процес має власний простір пам'яті. Тобто кожен отримує свій екземпляр синглтону. Більшість розробників натикаються на це вперше при деплої в балансоване середовище. Якщо потрібен справжній спільний стан між процесами, використовуй зовнішнє сховище типу Redis, а не синглтон. ### Типові помилки **Clusters ламають гарантію "одного екземпляру"** ```javascript // Кожен воркер-процес виведе різний PID const singleton = require('./singleton'); console.log(singleton.getInstance().id); // воркер 1: 1234, воркер 2: 1235 ``` Якщо застосунок запускає кілька Node.js процесів, синглтон не дає тобі одного спільного екземпляру. Для цього потрібен IPC або Redis. **Публічний конструктор дозволяє обійти захист** ```javascript // Неправильно class Config { constructor() { this.settings = {}; } // ніщо не зупиняє new Config() static getInstance() { /* ... */ } } const c1 = new Config(); // обходить getInstance const c2 = Config.getInstance(); // інший об'єкт ``` Зроби конструктор приватним (TypeScript) або кидай помилку всередині нього (чистий JS). **Тести ламаються через спільний стан** ```javascript // Забруднення тестів - Logger зберігає стан між тестами test('логує помилку', () => { Logger.getInstance().log('error'); }); test('лог порожній на старті', () => { // Logger ще тримає запис з попереднього тесту expect(Logger.getInstance().getLogs()).toHaveLength(0); // падає }); ``` Додай метод `resetInstance()` для тестового середовища або використовуй dependency injection замість голого синглтону. **Серіалізація падає через циклічне посилання** ```javascript JSON.stringify(Singleton.getInstance()); // Error: circular structure ``` Статичне поле `instance` вказує назад на сам об'єкт. Додай метод `toJSON()`, який повертає тільки потрібні дані. ### Де зустрічається в реальних проектах - **Winston** (Node логер): `createLogger()` повертає один логер на конфіг транспорту. - **Express**: об'єкт `app` - один екземпляр на серверний процес. - **Redux DevTools**: один екземпляр стора підключається до React-застосунку. - **React Context**: Provider створює одне значення, яке поширюється вниз по дереву компонентів, фактично синглтон для цього піддерева. - **Lodash**: утилітарний об'єкт є синглтоном на рівні модуля, але варто імпортувати окремі функції для tree-shaking. Коли потрібні тестовані, мокабельні залежності, подивись на InversifyJS або звичайний DI. Синглтон для справжніх унікальних ресурсів. ### Питання на співбесіді **Q:** Як зробити синглтон thread-safe у JavaScript? **A:** В однопотоковому JS простий `if (!instance)` достатній. З worker threads паралельні виклики `getInstance()` під час async-ініціалізації можуть змагатися. Використовуй `??=` для умовно атомарного присвоєння або захищай ініціалізацію прапором. **Q:** У чому різниця між синглтоном і глобальною змінною? **A:** Глобальна змінна - просто значення, яке будь-хто може перезаписати. Синглтон контролює створення (lazy init, рівно один раз) і може нести методи, валідацію та стан. Обидва мають проблему прихованих залежностей. **Q:** Як синглтон порушує SOLID? **A:** Порушує Single Responsibility (клас керує власним життєвим циклом І виконує свою пряму роботу) і Dependency Inversion (той, хто викликає, хардкодить `getInstance()` замість отримання інтерфейсу). **Q:** Як мокати синглтон у Jest? **A:** Використовуй `jest.spyOn(MyClass, 'getInstance').mockReturnValue(mockInstance)`. Або додай статичний `resetInstance()`, який очищає приватне поле, і викликай його в `beforeEach`. **Q:** (Senior) Чому синглтон для пулу з'єднань з БД - погана ідея в мікросервісах? **A:** Кожен pod масштабується незалежно і потребує власного пулу під своє навантаження. Синглтон всередині одного процесу - нормально, але припущення "один пул на весь застосунок" ламається, коли застосунок розбитий на 20 pods. Використовуй рядок підключення в конфізі та бібліотеку пулу типу `pg-pool` на кожен сервіс. ## Приклади ### Базовий: TypeScript синглтон для конфігу ```typescript class AppConfig { private static instance: AppConfig; private settings: Record<string, string> = {}; private constructor() { // Конфіг завантажується один раз this.settings = { env: process.env.NODE_ENV ?? 'development' }; } static getInstance(): AppConfig { if (!AppConfig.instance) { AppConfig.instance = new AppConfig(); } return AppConfig.instance; } get(key: string): string { return this.settings[key] ?? ''; } } const config1 = AppConfig.getInstance(); const config2 = AppConfig.getInstance(); console.log(config1 === config2); // true console.log(config1.get('env')); // 'development' ``` Обидві змінні тримають один і той самий об'єкт. `settings` завантажується рівно один раз, скільки б модулів не викликали `getInstance()`. ### Середній: Спільний логер в Express ```javascript class Logger { static #instance; #logs = []; constructor() { if (Logger.#instance) throw new Error('Використовуй Logger.getInstance()'); Logger.#instance = this; } static getInstance() { return Logger.#instance ?? (Logger.#instance = new Logger()); } log(message) { const entry = `${new Date().toISOString()}: ${message}`; this.#logs.push(entry); console.log(entry); } getLogs() { return this.#logs; } } // Маршрут A і маршрут B використовують один Logger і один масив #logs app.get('/users', (req, res) => { Logger.getInstance().log('GET /users'); res.json([]); }); app.post('/orders', (req, res) => { Logger.getInstance().log('POST /orders'); res.json({ id: 1 }); }); ``` Обидва маршрути викликають `getInstance()` і отримують один об'єкт, тому `#logs` накопичує записи з кожного запиту в одному місці. ### Senior: Чому тести ламаються без скидання стану ```javascript class Counter { static #instance; #count = 0; static getInstance() { return Counter.#instance ?? (Counter.#instance = new Counter()); } static resetInstance() { Counter.#instance = null; // тільки для тестів } increment() { this.#count++; } value() { return this.#count; } } // Тестовий файл beforeEach(() => Counter.resetInstance()); test('починається з нуля', () => { expect(Counter.getInstance().value()).toBe(0); // проходить щоразу }); test('інкрементує коректно', () => { Counter.getInstance().increment(); expect(Counter.getInstance().value()).toBe(1); // проходить щоразу }); ``` Без `resetInstance()` другий тест бачив би count рівний 1, залишений першим тестом. Спільний екземпляр - саме та причина, через яку синглтони забруднюють стан тестів, якщо не планувати це заздалегідь.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.