Как можно создать Singleton с помощью системы модульности в ноде?nodejs-56

В Node.js система модулей CommonJS по умолчанию предоставляет механизм для реализации паттерна Singleton благодаря особенностям кэширования модулей. Рассмотрим различные подходы к созданию Singleton.

Базовый механизм

Node.js кэширует модули после их первой загрузки. Это означает, что при повторных вызовах require() возвращается тот же экземпляр объекта.

// dbConnection.js
class DBConnection {
  constructor() {
    this.connection = null;
    console.log('DBConnection instance created');
  }

  connect() {
    this.connection = 'Connected to database';
    return this;
  }
}

// Экспортируем сразу инстанс класса
module.exports = new DBConnection().connect();

При использовании в разных файлах:

// file1.js
const db1 = require('./dbConnection');
console.log(db1.connection); // 'Connected to database'

// file2.js
const db2 = require('./dbConnection');
console.log(db2.connection); // 'Connected to database' (тот же инстанс)

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

Более контролируемый подход

Если нужно больше контроля над созданием инстанса:

// singleton.js
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }

    this.value = Math.random();
    Singleton.instance = this;
  }
}

module.exports = Singleton;

Использование:

const Singleton = require('./singleton');
const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // true
console.log(instance1.value === instance2.value); // true

Singleton с ленивой инициализацией

Для отложенного создания инстанса:

// lazySingleton.js
class LazySingleton {
  constructor() {
    this.value = null;
  }

  init() {
    if (!this.value) {
      this.value = Math.random();
    }
    return this;
  }
}

module.exports = new LazySingleton();

Использование:

const singleton = require('./lazySingleton');

// Инстанс создается только при вызове init()
console.log(singleton.value); // null
singleton.init();
console.log(singleton.value); // случайное число

Singleton с приватными полями

С использованием синтаксиса приватных полей класса:

// modernSingleton.js
class ModernSingleton {
  #value; // Приватное поле

  constructor() {
    if (ModernSingleton.#instance) {
      return ModernSingleton.#instance;
    }

    this.#value = Math.random();
    ModernSingleton.#instance = this;
  }

  static #instance = null;

  get value() {
    return this.#value;
  }
}

module.exports = ModernSingleton;

Singleton с зависимостями

Пример с подключением к базе данных:

// database.js
const { Client } = require('pg');

class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }

    this.client = new Client({
      connectionString: process.env.DATABASE_URL
    });

    this.isConnected = false;
    Database.instance = this;
  }

  async connect() {
    if (!this.isConnected) {
      await this.client.connect();
      this.isConnected = true;
    }
    return this;
  }
}

module.exports = new Database();

Особенности Node.js модулей

  1. Кэширование модулей: Node.js кэширует модули по абсолютному пути к файлу
  2. Глобальность: Singleton действительно глобальный только в пределах одного процесса Node.js
  3. Тестирование: Singleton может усложнить тестирование из-за сохранения состояния

Потенциальные проблемы

  1. Тестирование: Сложно изолировать тесты друг от друга
  2. Горячая перезагрузка: При hot-reload состояние Singleton сохраняется
  3. Многопоточность: В кластерном режиме каждый процесс имеет свой экземпляр

Резюмируем:

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