Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Singleton: Паттерн проектирования и его подводные камни
Singleton — это паттерн проектирования, который гарантирует, что класс имеет только один экземпляр и предоставляет глобальную точку доступа к этому экземпляру. Несмотря на популярность, это один из наиболее критикуемых паттернов в современной разработке.
Классическая реализация Singleton
// ❌ Базовый Singleton (не потокобезопасный)
class Database {
private static instance: Database | null = null;
private connections: Array<any> = [];
private constructor() {
// Private конструктор предотвращает создание через new
}
static getInstance(): Database {
if (Database.instance === null) {
Database.instance = new Database();
}
return Database.instance;
}
connect(config: any): void {
this.connections.push(config);
}
}
// Использование
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true — один экземпляр
Проблемы базового Singleton
1. Race Condition в многопоточной среде
// ❌ ПЛОХО: race condition
static getInstance(): Database {
if (Database.instance === null) { // Thread A проверяет
// Переключение контекста на Thread B
Database.instance = new Database(); // Thread A создает
// Переключение контекста на Thread B
}
if (Database.instance === null) { // Thread B проверяет
Database.instance = new Database(); // Thread B создает (второй экземпляр!)
}
return Database.instance;
}
2. Тестируемость
// ❌ ПЛОХО: нельзя мокировать Singleton в тестах
class UserService {
private db = Database.getInstance(); // Hard-coded dependency
async getUser(id: string) {
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
// Тест
test('getUser', async () => {
// Как мокировать? Database.getInstance() всегда возвращает реальный экземпляр
const service = new UserService();
// Нельзя контролировать, что вернет Database
const user = await service.getUser('123');
});
3. Глобальное состояние
// ❌ ПЛОХО: глобальное состояние — причина багов
class Logger {
private static instance: Logger = new Logger();
private logs: string[] = [];
static getInstance(): Logger {
return Logger.instance;
}
log(message: string): void {
this.logs.push(message);
}
}
// Где-то в коде A
Logger.getInstance().log('Error in module A');
// Где-то в коде B
Logger.getInstance().log('Error in module B');
// На другом конце приложения
const logger = Logger.getInstance();
console.log(logger.logs); // Какой порядок? Какой контекст? Хаос!
Улучшенные реализации
Потокобезопасный Singleton (Eager Initialization)
// ✅ ХОРОШО: создаем экземпляр при загрузке модуля
class Database {
private static readonly instance = new Database();
private connections: Array<any> = [];
private constructor() {
console.log('Database initialized');
}
static getInstance(): Database {
return Database.instance;
}
}
// Создание происходит сразу
const db = Database.getInstance();
Ленивая инициализация (Lazy Initialization) с замком
// ✅ ХОРОШО: потокобезопасный Singleton
class Database {
private static instance: Database | null = null;
private static lock = new Promise<void>(resolve => resolve());
private constructor() {}
static async getInstance(): Promise<Database> {
if (Database.instance === null) {
await Database.lock;
if (Database.instance === null) {
Database.instance = new Database();
}
}
return Database.instance;
}
}
Модульный Singleton (рекомендуемый подход)
// ✅ РЕКОМЕНДУЕТСЯ: Singleton через модуль
// database.ts
class Database {
private connections: Array<any> = [];
connect(config: any): void {
this.connections.push(config);
}
getConnections() {
return this.connections;
}
}
// Создаем экземпляр один раз
const database = new Database();
// Экспортируем уже созданный экземпляр
export const db = database;
// ============ Использование ==============
// service.ts
import { db } from './database';
class UserService {
async getUser(id: string) {
return db.getConnections()[0].query('SELECT * FROM users WHERE id = ?', [id]);
}
}
Преимущества модульного подхода:
- Нет необходимости в static методах
- Легко мокировать в тестах
- Понятная зависимость через imports
- Работает в Node.js из коробки (благодаря require/import кешированию)
Dependency Injection — современное решение
// ✅ ЛУЧШИЙ ПОДХОД: Dependency Injection
// Вместо Singleton используем DI контейнер
import { Injectable } from '@nestjs/common';
// Database — просто класс, без Singleton логики
@Injectable()
class Database {
connect(config: any) {
console.log('Connecting to', config);
}
}
// Service получает зависимость через конструктор
@Injectable()
class UserService {
constructor(private db: Database) {} // Инъекция!
async getUser(id: string) {
// Используем this.db
}
}
// NestJS контейнер управляет экземплярами
// Автоматически создает один экземпляр Database и переиспользует его
const app = await NestFactory.create(AppModule);
Преимущества DI:
- Нет Singleton логики в классе
- Легко тестировать (мокируем через конструктор)
- Гибкое управление жизненным циклом
- Явные зависимости
Где Singleton может быть полезен
1. Кеширование
class CacheManager {
private static instance: CacheManager;
private cache = new Map<string, any>();
private constructor() {}
static getInstance(): CacheManager {
if (!CacheManager.instance) {
CacheManager.instance = new CacheManager();
}
return CacheManager.instance;
}
set(key: string, value: any): void {
this.cache.set(key, value);
}
get(key: string): any | undefined {
return this.cache.get(key);
}
}
// Использование
const cache = CacheManager.getInstance();
cache.set('user:123', { id: 123, name: 'John' });
2. Конфигурация приложения
class Config {
private static instance: Config;
private config: Record<string, any> = {};
private constructor() {
this.loadConfig();
}
static getInstance(): Config {
if (!Config.instance) {
Config.instance = new Config();
}
return Config.instance;
}
private loadConfig(): void {
this.config = {
db_host: process.env.DB_HOST,
db_port: process.env.DB_PORT,
api_key: process.env.API_KEY
};
}
get(key: string): any {
return this.config[key];
}
}
3. Логирование
class Logger {
private static instance: Logger;
private logFile: fs.WriteStream;
private constructor() {
this.logFile = fs.createWriteStream('app.log', { flags: 'a' });
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(level: string, message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${level}: ${message}\n`;
this.logFile.write(logEntry);
}
}
Сравнение подходов
const approaches = {
singleton: {
pros: ['Гарантирует один экземпляр', 'Простая реализация'],
cons: ['Сложно тестировать', 'Глобальное состояние', 'Hard-coded зависимости']
},
module_pattern: {
pros: ['Просто', 'Работает в Node.js', 'Легко мокировать'],
cons: ['Нет явного контроля жизненного цикла']
},
dependency_injection: {
pros: ['Гибко', 'Легко тестировать', 'Явные зависимости', 'Управление жизненным циклом'],
cons: ['Требует framework/контейнер', 'Больше boilerplate']
}
};
Вывод
В современной разработке Singleton редко используется напрямую. Вместо этого рекомендуется:
- Для Node.js: использовать модульный паттерн (module.exports)
- Для приложений: использовать Dependency Injection (NestJS, Angular, Spring)
- Для редких случаев: использовать модульный Singleton, если DI недоступен
Singleton решает реальную проблему (один экземпляр), но создает больше проблем (тестируемость, глобальное состояние), чем решает.