7. Singleton
Design Patterns. Урок: Singleton (Одиночка)
Заголовок раздела «Design Patterns. Урок: Singleton (Одиночка)»Singleton — порождающий паттерн, гарантирующий что у класса есть только один экземпляр, и предоставляющий глобальную точку доступа к нему.
Когда нужен Singleton?
Заголовок раздела «Когда нужен Singleton?»- Подключение к базе данных (один connection pool)
- Конфигурация приложения (один объект настроек)
- Логгер (единая точка записи логов)
- Кеш (единое хранилище)
- Event bus (единый шина событий)
Классическая реализация
Заголовок раздела «Классическая реализация»class DatabaseConnection { private static instance: DatabaseConnection | null = null; private connection: any;
// Приватный конструктор — никто не может создать экземпляр снаружи private constructor() { this.connection = createDatabaseConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), database: process.env.DB_NAME, }); console.log('Database connection created'); }
// Единственный способ получить экземпляр static getInstance(): DatabaseConnection { if (!DatabaseConnection.instance) { DatabaseConnection.instance = new DatabaseConnection(); } return DatabaseConnection.instance; }
query(sql: string, params?: any[]): Promise<any> { return this.connection.execute(sql, params); }}
// Использованиеconst db1 = DatabaseConnection.getInstance();const db2 = DatabaseConnection.getInstance();console.log(db1 === db2); // true — один и тот же объектThread-safe Singleton (для Node.js worker threads)
Заголовок раздела «Thread-safe Singleton (для Node.js worker threads)»class ConfigManager { private static instance: ConfigManager | null = null; private static isCreating = false; private config: Record<string, any> = {};
private constructor() { this.loadConfig(); }
static getInstance(): ConfigManager { if (!ConfigManager.instance) { if (ConfigManager.isCreating) { throw new Error('Circular dependency detected in ConfigManager'); } ConfigManager.isCreating = true; ConfigManager.instance = new ConfigManager(); ConfigManager.isCreating = false; } return ConfigManager.instance; }
private loadConfig(): void { this.config = { apiUrl: process.env.API_URL ?? 'http://localhost:3000', debug: process.env.DEBUG === 'true', maxRetries: Number(process.env.MAX_RETRIES ?? 3), }; }
get<T>(key: string): T { return this.config[key] as T; }
set(key: string, value: any): void { this.config[key] = value; }}
// Использованиеconst config = ConfigManager.getInstance();const apiUrl = config.get<string>('apiUrl');Современный подход: модульный Singleton
Заголовок раздела «Современный подход: модульный Singleton»В Node.js каждый модуль кешируется при первом import — это встроенный Singleton:
// logger.ts — это уже Singleton благодаря модульной системе Node.jsclass Logger { private logs: string[] = [];
log(message: string): void { const entry = `[${new Date().toISOString()}] ${message}`; this.logs.push(entry); console.log(entry); }
getLogs(): string[] { return [...this.logs]; }}
// Экспортируем единственный экземплярexport const logger = new Logger();
// В любом файле проекта:import { logger } from './logger';logger.log('App started'); // Всегда один и тот же экземплярSingleton vs Global Variable
Заголовок раздела «Singleton vs Global Variable»Singleton — не просто глобальная переменная. Ключевые отличия:
| Аспект | Global Variable | Singleton |
|---|---|---|
| Контроль создания | ❌ Создаётся сразу | ✅ Ленивая инициализация |
| Защита от множественных экземпляров | ❌ Нет | ✅ Есть |
| Наследование и расширение | ❌ Сложно | ✅ Возможно |
| Тестирование | ❌ Трудно мокать | ✅ Можно сбросить instance |
Проблемы Singleton и как их избежать
Заголовок раздела «Проблемы Singleton и как их избежать»Проблема 1: Тяжело тестировать
// Решение: добавь возможность сброса для тестовclass EventBus { private static instance: EventBus | null = null;
static getInstance(): EventBus { if (!EventBus.instance) { EventBus.instance = new EventBus(); } return EventBus.instance; }
// Только для тестов! static resetInstance(): void { if (process.env.NODE_ENV === 'test') { EventBus.instance = null; } }}Проблема 2: Скрытые зависимости
// ❌ Синглтон внутри функции — скрытая зависимостьfunction processOrder(order: Order): void { const db = DatabaseConnection.getInstance(); // Скрыто! db.query('...');}
// ✅ Явная зависимость через DIPfunction processOrder(order: Order, db: Database): void { db.query('...');}Singleton в WordPress
Заголовок раздела «Singleton в WordPress»// WordPress часто использует Singleton для плагиновclass MyPlugin { private static ?MyPlugin $instance = null;
public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; }
private function __construct() { add_action('init', [$this, 'init']); add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); }
public function init(): void { /* ... */ } public function enqueueScripts(): void { /* ... */ }}
// ИнициализацияMyPlugin::getInstance();Практические задания
Заголовок раздела «Практические задания»-
Реализуй
CacheManager— Singleton с методамиget(key),set(key, value, ttlMs),delete(key),clear(). -
Создай
EventBusSingleton с методамиon(event, handler),off(event, handler),emit(event, data). -
Напиши тест для Singleton: убедись что
getInstance()всегда возвращает один объект. -
Чем отличается Singleton от статического класса (все методы static)? Когда что использовать?
-
Изучи паттерн «Multiton» — обобщение Singleton для заданного количества экземпляров.