Singleton Pattern (GoF) — Синглтон

Гарантия одного экземпляра класса на процесс. Автор: «на JS почти бессмысленная вещь» — но знать обязательно для других языков и для понимания ES-модулей.

Проблема

В системе должен существовать ровно один объект с разделяемым состоянием: пул соединений к БД, конфигурация, логгер, event bus. Без паттерна каждый new создаёт отдельный экземпляр.

Где используется

  • Database connection pool
  • Конфигурация приложения (один объект settings)
  • Логгер (один централизованный логгер)
  • Event bus, store (Redux store — синглтон)
  • Кэш приложения
  • Модальные окна (один на страницу)

Решение

В JS — три уровня надёжности:

  1. Через свойство конструктора — простой, но взламывается извне (Singleton.instance = null).
  2. Через замыкание (IIFE) — instance в функциональном контексте, недоступен снаружи.
  3. Через ES-модульexport default new X. Модуль грузится один раз, экземпляр кэшируется.

Реализации

Через класс со статическим полем

class Singleton {
  static _instance = null;

  constructor(config) {
    if (Singleton._instance) return Singleton._instance;
    this.config = config;
    Singleton._instance = this;
  }

  static getInstance(config) {
    if (!Singleton._instance) {
      Singleton._instance = new Singleton(config);
    }
    return Singleton._instance;
  }
}

const a = new Singleton({ env: 'production' });
const b = new Singleton({ env: 'development' }); // игнорируется
a === b; // true

Через замыкание (IIFE)

const Singleton = (() => {
  let instance;
  return class {
    constructor {
      if (instance) return instance;
      instance = this;
    }
  };
});

Через ES Module (рекомендуемый для JS)

// config.js — модуль сам по себе singleton
// Модуль выполняется один раз, все импорты получают тот же объект
class AppConfig {
  constructor {
    this.env = process.env.NODE_ENV || 'development';
    this.apiUrl = process.env.API_URL || 'http://localhost:3000';
  }
  get(key) { return this[key]; }
}
export const config = new AppConfig();

// app.js, logger.js — везде один и тот же config
import { config } from './config.js';

Lazy Singleton — Pool

const DatabasePool = (() => {
  let instance = null;
  class Pool {
    constructor(maxConnections) {
      this.maxConnections = maxConnections;
      this.connections = ;
    }
    query(sql) { /* ... */ }
  }
  return {
    getInstance(max = 10) {
      if (!instance) instance = new Pool(max);
      return instance;
    }
  };
});

С возможностью reset (для тестов)

class Logger {
  static _instance = null;
  constructor(level = 'info') { this.level = level; this.logs = ; }
  log(msg) { this.logs.push({ level: this.level, msg }); }

  static getInstance(level) {
    if (!Logger._instance) Logger._instance = new Logger(level);
    return Logger._instance;
  }
  static reset { Logger._instance = null; } // только для тестов!
}

Где используется в JS-экосистеме

  • Redux store — синглтон по умолчанию
  • Database pool — один pool на приложение
  • Конфигурацияexport const config = { ... }
  • Любой export default new X в Node.js

Подводные камни

  • Часто антипаттерн: shared state, скрытые зависимости, усложняет тесты.
  • Не разделяется между worker_threads / Web Workers — каждый поток создаёт свой.
  • В тестах нужен reset или DI вместо синглтона.
  • Контрол не полный в чистом JS: публичное instance-свойство можно сбросить извне — нужно замыкание.
  • Не защищён от наследования в простой реализации:
    class Base {
      constructor { if (Base.instance) return Base.instance; Base.instance = this; }
    }
    class Child extends Base {}
    new Base === new Child(); // true — но b ожидался Child!
    // Решение: проверять new.target
    
  • Порядок инициализации модулей: если singleton зависит от другого singleton, порядок import может вызвать проблемы.
  • Multiton — расширение: ровно N инстансов, ключ → инстанс.

Главные тезисы автора

  • «На JavaScript это почти бессмысленная вещь» — модули уже сами синглтоны.
  • Часто относят к антипаттернам из-за shared state.
  • «В JavaScript классно использовать систему модульности для создания синглтонов».
  • Не безопасен без замыкания: публичное instance-свойство можно сбросить извне.
  • Object Pool — расширение синглтона с ограничением (multiton + переиспользование).

🎓 Источники

См. также