Синглтоны – это классы, которые могут создавать только один экземпляр и могут быть доступны глобально. Этот единственный экземпляр может быть общим для всего нашего приложения, что делает Синглтоны отличным способом хранить глобальный стейт в приложении.
Для начала, давайте посмотрим, как может выглядеть Синглтон с использованием 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.