Что такое паттерн Singleton?

Ответ

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

Зачем он нужен в Node.js? В Node.js модули по умолчанию являются синглтонами (кэшируются после первого require). Однако паттерн полезен, когда нужно явно контролировать процесс создания единственного экземпляра класса, например, для управления подключением к базе данных, конфигурацией или логгером.

Классическая реализация в JavaScript (ES6+):

class DatabaseConnection {
  constructor(config) {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }
    // Имитация сложной инициализации подключения
    this.connectionString = config.connectionString;
    this.isConnected = false;
    this.connect();
    DatabaseConnection.instance = this;
  }

  connect() {
    console.log(`Устанавливается подключение к ${this.connectionString}...`);
    // Логика подключения...
    this.isConnected = true;
  }

  query(sql) {
    if (!this.isConnected) throw new Error('Нет подключения к БД');
    console.log(`Выполняется запрос: ${sql}`);
    // Логика выполнения запроса...
  }

  // Статический метод для удобного доступа (опционально)
  static getInstance(config) {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(config);
    }
    return DatabaseConnection.instance;
  }
}

// Использование
const config = { connectionString: 'postgresql://localhost/mydb' };

const db1 = new DatabaseConnection(config); // Создаст экземпляр
const db2 = new DatabaseConnection(config); // Вернет существующий экземпляр
const db3 = DatabaseConnection.getInstance(config); // Вернет тот же экземпляр через статический метод

console.log(db1 === db2 && db2 === db3); // true

Особенности в Node.js:

  • Благодаря модульной системе, часто достаточно экспортировать один экземпляр из файла модуля:
    // logger.js
    class Logger { /* ... */ }
    module.exports = new Logger(); // Все, кто require('./logger'), получат один экземпляр
  • В Node.js нет проблемы многопоточности, поэтому реализация проще, чем в Java/C#.

Недостатки:

  • Усложняет модульное тестирование: Глобальное состояние может приводить к зависимостям между тестами. Решается использованием dependency injection.
  • Скрывает зависимости: Класс, использующий синглтон, имеет скрытую зависимость, что нарушает принцип инверсии зависимостей.

Вывод: Используйте синглтон осознанно, в основном для истинно глобальных, неделимых ресурсов, и помните о его влиянии на тестируемость кода.

Ответ 18+ 🔞

А, синглтон, ёпта! Ну это же классика, как водка с пивом — все знают, что вредно, но иногда без этого нихуя не получается.

Смотри, в чём прикол. Этот паттерн — такой хитрожопый сторож, который стоит у двери и орет: «Э, куда прешь? Второй экземпляр? Иди нахуй, один уже внутри!». Он гарантирует, что у какого-то важного класса будет всего один-единственный экземпляр на всю твою программу, и все будут ходить к нему на поклон, как к общему кошельку на тусовке.

Зачем он в Node.js, если и так всё — синглтон? Ну да, модули в Node кэшируются, require один раз — и всё. Но представь, что ты делаешь не просто модуль, а, например, подключение к базе данных. Ты же не хочешь, чтобы каждый файл в приложении создавал своё собственное соединение, жрал все лимиты и потом приложение накрывалось медным тазом? Вот тут синглтон и выручает — один коннект на всех, и все им пользуются. То же самое с логгером или глобальной конфигурацией.

Смотри, как это выглядит в коде, если делать по-взрослому:

class DatabaseConnection {
  constructor(config) {
    // Вот этот момент — ключевой, блядь!
    // Если экземпляр уже есть — выплевываем его обратно, новый не создаем.
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }
    // А если нет — тогда начинаем ебаться с инициализацией.
    this.connectionString = config.connectionString;
    this.isConnected = false;
    this.connect();
    // И кладём только что созданного себя в статическое поле — вот он, наш единственный и неповторимый.
    DatabaseConnection.instance = this;
  }

  connect() {
    console.log(`Устанавливается подключение к ${this.connectionString}...`);
    // Тут типа долгая и ресурсоёмкая хуйня...
    this.isConnected = true;
  }

  query(sql) {
    if (!this.isConnected) throw new Error('Нет подключения к БД');
    console.log(`Выполняется запрос: ${sql}`);
    // Логика выполнения запроса...
  }

  // Можно ещё сделать статический метод для красоты, чтобы не через new вызывать
  static getInstance(config) {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(config);
    }
    return DatabaseConnection.instance;
  }
}

// Использование
const config = { connectionString: 'postgresql://localhost/mydb' };

const db1 = new DatabaseConnection(config); // Первый раз — создастся, овердохуища работы.
const db2 = new DatabaseConnection(config); // Второй раз — получишь ту же самую ссылку, что и в db1.
const db3 = DatabaseConnection.getInstance(config); // И через метод — тоже самое.

console.log(db1 === db2 && db2 === db3); // true — один и тот же объект, ёба!

Но в Node.js часто проще, чувак. Зачем городить огород с проверкой в конструкторе? Можно просто в модуле создать экземпляр и экспортировать его. Все, кто его импортит, получат один и тот же объект. Проще пареной репы.

// logger.js
class Logger { /* ... */ }
module.exports = new Logger(); // Всё, приехали. Теперь это синглтон де-факто.

А теперь про недостатки, потому что нихуя не всё так радужно. Главная проблема — тестирование. Это пиздец, как неудобно. Представь, у тебя глобальное состояние, которое тянется из теста в тест. Один тест что-то поменял в синглтоне, а следующий уже ебётся с последствиями. Доверия к такому коду — ноль ебать. Умные дяди говорят, что это нарушает кучу принципов, зависимости становятся скрытыми, и в целом архитектура начинает напоминать говно.

Так что вывод, блядь, простой. Используй синглтон, только когда это реально оправдано — для чего-то по-настоящему глобального и неделимого, типа того же подключения к БД. А во всех остальных случаях лучше подумать о нормальных фабриках или dependency injection, чтобы не охуеть потом при рефакторинге.