← Назад к вопросам

Что такое Singleton?

2.0 Middle🔥 241 комментариев
#Архитектура и паттерны

Комментарии (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 редко используется напрямую. Вместо этого рекомендуется:

  1. Для Node.js: использовать модульный паттерн (module.exports)
  2. Для приложений: использовать Dependency Injection (NestJS, Angular, Spring)
  3. Для редких случаев: использовать модульный Singleton, если DI недоступен

Singleton решает реальную проблему (один экземпляр), но создает больше проблем (тестируемость, глобальное состояние), чем решает.