Singleton Pattern

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

Для начала, давайте посмотрим, как может выглядеть Синглтон с использованием ES6. Для примера мы создадим класс Counter, который имеет:

  • метод getInstance, который возвращает значение экземпляра
  • метод getCount, который возвращает текущее значение переменной counter
  • метод increment, который увеличивает counter на единицу
  • метод decrement, который уменьшает counter на единицу
let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

Как мы видим, этот класс не является Синглтоном, так как Синглтон должен быть единственным экземпляром. В этом же классе мы можем создать множество экземпляров класса Counter.

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

Вызывая метод new дважды, мы присваиваем counter1 и counter2 к разным экземплярам. Значения, возвращаемые методом getInstance в объектах counter1 и counter2, ссылаются на разные экземпляры, которые не равны при строгом сравнении.

Давайте убедимся, что мы создаем только один экземпляр класса Counter.

Один из способов создания только одного экземпляра – это использование переменной instance. В конструкторе Counter, мы можем присвоить instance к только что созданному экземпляру. Далее, мы можем предотвращать создание новых инстансов, проверяя переменную instance на наличие значения. В таком случае, экземпляр уже существует. В таком случае будем выбрасывать ошибку.

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

Отлично, теперь мы не можем создать несколько экземпляров.

Так же, для предотвращения возможного изменения Синглтона в дальнейшем, желательно воспользоваться Object.freeze. Свойства frozen экземпляра не смогут быть изменены, что уменьшит риск перезаписи полей нашего Синглтона.

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

Плюсы и минусы

Возможность создавать только один экземпляр, вместо множества, может помочь в экономии памяти. Вместо выделения памяти каждый раз при создании нового экземпляра, мы выделяем память только для одного экземпляра, который в дальнейшем используем во всем приложении. Однако, Синглтоны считаются анти-паттерном и, желательно, по возможности их избегать в Javascript.

Во многих языках программирования, таких как Java или c++, невозможно создавать объекты напрямую, как мы это делаем в JavaScript. В этих объектно-ориентированных языках, нам надо создать класс, при помощи которого мы сможем создавать объекты. Эти созданные объекты имеют значение экземпляра класса, как значение instance в примере JavaScript.

Однако, пример с классом выше это, на самом деле, излишнее усложнение. Так как мы можем напрямую создавать объекты в JavaScript, мы просто можем использовать обычный объект, чтобы достичь тех же результатов. Давайте рассмотрим несколько минусов использования Синглтонов.

Использование обычных объектов

Давайте возьмем пример выше и переделаем его с использованием обычных объектов. Теперь counter это простой объект, содержащий:

  • свойство count
  • метод increment, увеличивающий count на 1
  • метод decrement, уменьшающий count на 1
let count = 0;

const counter = {
  increment() {
    return ++count;
  },
  decrement() {
    return --count;
  }
};

Object.freeze(counter);
export { counter };

Теперь, если мы импортируем этот объект в разные файлы и присвоим его разным объектам, то изменения count при использовании методов increment или decrement, будут одинаковые для этих объектов, т.к. присваивание объектов идет по ссылке и они будут ссылаться на один объект counter.

Глобальное поведение

Экземпляр Синглтона может быть использован по всему приложению. Глобальные переменные имеют похожее поведение: так как они видны в глобальной области видимости, мы можем иметь к ним доступ во всем приложении.

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

В ES2015, создание глобальных переменных встречается все реже. Новые объявления let и const предотвращают случайные изменения в глобальной области видимости, сохраняя область видимости переменных внутри блока. Новая система module в JavaScript создает возможность простого доступа к глобальным переменным, без риска загрязнения глобальной области, используя export переменных из модуля и import этих переменных в других файлах.

Однако, основным использованием Синглтона является хранение некоторого глобального состояния приложения. Использование мутабельного объекта для хранения такого состояния может привести к проблемам.

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

Управление состоянием в React

В React довольно мы часто работаем с глобальным состоянием используя такие утилиты как Redux или React Context, вместо Синглтонов. И, хотя их поведение чем-то напоминает Синглтон, эти утилиты предоставляют read-only state, в отличие от мутабельного состояния Синглтона. В Redux только чистые функции (pure functions) редьюсеров могут обновлять состояние, после того как компонент пошлет action через dispatch.

  •  
  •  
  •  
  •  
  •  
  •