Как их реализовать и действительно ли они полезны

Синглтон - это функция или класс, который может иметь только один экземпляр. Это шаблон дизайна, популяризированный «Бандой четырех» в их влиятельных шаблонах дизайна. Однако синглтон - это один из нескольких раскритикованных шаблонов в книге. Некоторые из них считаются антипаттернами, в то время как другие полезны только в определенных языках. Итак, что насчет синглтонов? И имеют ли они смысл в JavaScript?

В этой статье мы увидим, как реализовать синглтоны в JavaScript, прежде чем спросить, полезны ли они для этого языка.

Класс Синглтон

Поскольку Шаблоны проектирования ориентированы на C ++, мы привыкли рассматривать синглтоны как классы. Начиная с ES6, у нас был синтаксис класса в JavaScript, поэтому базовая реализация синглтона могла бы выглядеть примерно так:

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

Функция Singleton

Если нам нужна обратная совместимость или мы предпочитаем функции классам, мы можем сделать это с помощью очень небольших настроек приведенного выше примера.

function Singleton() {
  if (!Singleton._instance) {
    Singleton._instance = this;
  }
  return Singleton._instance;
}

Доступ к экземпляру

Используем ли мы класс или функцию, каждый раз, когда мы вызываем new Singleton(), мы будем обращаться к одному и тому же экземпляру. Но это может легко запутать: это не ожидаемое поведение для ключевого слова new.

По этой причине часто используется getInstance метод для самого класса или функции.

Класс getInstance

class Singleton {
  constructor() {
    if (!Singleton._instance) {
      Singleton._instance = this;
    }
    return Singleton._instance;
  }
  static getInstance() {
    return this._instance;
  }
}

Создав экземпляр с new Singleton(), мы можем вызвать Singleton.getInstance().

Функция getInstance

function Singleton() {
  if (!Singleton._instance) {
    Singleton._instance = this;
  }
  
  Singleton.getInstance = function () {
    return this._instance;
  };
  return Singleton._instance;
}

После того, как мы вызвали функцию Singleton(), мы можем вызвать Singleton.getInstance(). Теперь мы знаем, как создавать синглтоны, должны ли мы действительно использовать их в нашем коде?

Аргумент в пользу

В JavaScript есть одно главное техническое преимущество синглтона: отложенная загрузка. В отличие от глобальных переменных, наш синглтон будет создан только тогда, когда он нам понадобится.

Будет ли это значимым преимуществом not, будет зависеть от вашего конкретного варианта использования. Если синглтон содержит значения с отслеживанием состояния, создание экземпляра занимает много времени или памяти и, следовательно, глобальная переменная будет блокировать другой более важный код, тогда вам может быть лучше с синглтон.

Аргумент против

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

Представьте, что вы хотите создать Logger службу, которая отправляет журналы в сторонний инструмент управления журналами. Вам необходимо отправлять журналы по всему приложению, но вы хотите авторизовать стороннюю интеграцию только один раз. Синглтон, немного похожий на приведенный ниже, может показаться разумным решением:

class Logger {
  constructor(config) {
    if (!Logger._instance) {
      Logger._instance = this;
      authenticateLogger(config);
    }
    return Logger._instance;
  }
  log(...logs) {
    sendLogsToLogManagementTool(...logs);   
    console.log(...logs);
  }
  static getInstance() {
    return this._instance;
  }
}
export default Logger;

Теперь, когда разработчик пытается создать new Logger, ненужный код запускаться не будет, потому что мы по-прежнему будем работать только с одним экземпляром.

Но что, если разработчик не осознает, что использует синглтон? Когда мы определили наш класс, могло показаться, что в будущем не будет необходимости расширять функцию Logger или создавать второй экземпляр.

Но на самом деле довольно легко придумать сценарий: что, если бы мы решили, что хотим, чтобы определенная часть кодовой базы была связана с разными учетными данными в нашем инструменте ведения журнала? Возможно, разные команды захотят отслеживать разные части приложения по отдельности.

Вместо этого мы могли бы просто экспортировать экземпляр класса Logger:

class Logger {
  constructor(config) {
    authenticateLogger(config);
  }
  log(...logs) {
    sendLogsToLogManagementTool(...logs);   
    console.log(...logs);
  }
}
const logger = new Logger(config);
export default logger;

С помощью этого решения:

  • Наш класс ведет себя так, как ожидалось, поэтому нет риска путаницы или неправильного использования,
  • Его легко расширяют другие разработчики,
  • Это упрощает модульное тестирование класса и отдельных экземпляров,
  • Код класса становится немного менее сложным, поскольку нам больше не нужен getInstance метод.

Преимущество отложенной загрузки может быть потеряно, но наша функция authenticateLogger в любом случае вряд ли будет так требовательна к производительности.

Что вы думаете? Есть ли место для синглтонов в JavaScript?