В этой статье мы рассмотрим лучший способ реализации синглтона в JavaScript и рассмотрим, как это развивалось с появлением ES6.
Среди языков, используемых в широко распространенном производстве, JavaScript, безусловно, наиболее быстро развивается, он выглядит не так, как его самые ранние итерации и больше похож на Python, с каждой новой спецификацией, выдвигаемой ECMA International. Несмотря на то, что изменения имеют свою значительную долю недоброжелателей, новый JavaScript действительно делает код более легким для чтения и рассуждений, более простым для написания таким образом, чтобы соответствовать передовым методам разработки программного обеспечения (особенно концепциям модульности и принципам SOLID), и проще собрать в канонические шаблоны проектирования программного обеспечения.
Объясняя ES6
ES6 (он же ES2015) был первым крупным обновлением языка с тех пор, как ES5 была стандартизирована в 2009 году. Почти все современные браузеры поддерживают ES6 . Однако, если вам нужно приспособить старые браузеры, код ES6 можно легко перенести в ES5 с помощью такого инструмента, как Babel. ES6 предоставляет JavaScript массу новых функций, включая улучшенный синтаксис для классов и новые ключевые слова для объявлений переменных . Вы можете узнать больше об этом, просматривая статьи SitePoint на эту тему .
Что такое синглтон
Если вы не знакомы с одноэлементным шаблоном , то по своей сути это шаблон проектирования, ограничивающий создание экземпляра класса одним объектом. Обычно целью является управление глобальным состоянием приложения. Некоторые примеры, которые я видел или написал сам, включают использование синглтона в качестве источника настроек конфигурации для веб-приложения на стороне клиента для всего, что инициировано с помощью ключа API (обычно вы не хотите рисковать отправкой нескольких вызовов отслеживания аналитики, например), и хранить данные в памяти в веб-приложении на стороне клиента (например, хранит в Flux).
Синглтон должен быть неизменяемым потребляющим кодом, и не должно быть опасности создания экземпляра более чем одного из них.
Примечание: есть сценарии, когда синглеты могут быть плохими, и аргументы, что они, на самом деле, всегда плохие. Для этого обсуждения вы можете проверить эту полезную статью на эту тему.
Старый способ создания синглтона в JavaScript
Старый способ написания синглтона в JavaScript предполагает использование замыканий и немедленное использование выражений функций . Вот как мы можем написать (очень простое) хранилище для гипотетической реализации Flux по-старому:
var UserStore = (function(){
var _data = [];
function add(item){
_data.push(item);
}
function get(id){
return _data.find((d) => {
return d.id === id;
});
}
return {
add: add,
get: get
};
}());
Когда этот код интерпретируется, UserStore
Однако этот код более многословен, чем должен быть, и также не дает нам неизменности, которую мы желаем при использовании синглетонов. Код, выполненный позже, может изменить любую из представленных функций или даже полностью переопределить UserStore
Более того, модифицирующий / оскорбляющий код может быть где угодно! Если мы получили ошибки в результате неожиданной модификации UsersStore
Есть более продвинутые шаги, которые вы можете использовать, чтобы смягчить некоторые из этих недостатков, как указано в этой статье Бена Черри. (Его цель состоит в том, чтобы создавать модули, которые оказываются одиночными, но шаблон тот же.) Но они добавляют ненужную сложность к коду, но так и не дают нам именно то, что мы хотим.
Новый путь
Используя возможности ES6, главным образом модули и новое объявление переменных const
Давайте начнем с самой базовой реализации. Вот (более чистая и мощная) современная интерпретация приведенного выше примера:
const _data = [];
const UserStore = {
add: item => _data.push(item),
get: id => _data.find(d => d.id === id)
}
Object.freeze(UserStore);
export default UserStore;
Как видите, этот способ улучшает читаемость. Но где он действительно сияет, так это в ограничении, накладываемом на код, который использует наш маленький одноэлементный модуль: потребляющий код не может переназначить UserStore
const
И в результате нашего использования Object.freeze его методы не могут быть изменены, и новые методы или свойства не могут быть добавлены к нему. Кроме того, поскольку мы используем преимущества модулей ES6, мы точно знаем, где используется UserStore
Теперь мы сделали UserStore
В большинстве случаев использование литерала объекта является наиболее читабельным и лаконичным вариантом. Однако бывают случаи, когда вы захотите воспользоваться преимуществами традиционного занятия. Например, магазины во Flux будут иметь одинаковую базовую функциональность. Использование традиционного объектно-ориентированного наследования — один из способов получить эту повторяющуюся функциональность, сохраняя при этом ваш код СУХИМЫМ.
Вот как будет выглядеть реализация, если мы хотим использовать классы ES6:
class UserStore {
constructor(){
this._data = [];
}
add(item){
this._data.push(item);
}
get(id){
return this._data.find(d => d.id === id);
}
}
const instance = new UserStore();
Object.freeze(instance);
export default instance;
Этот способ немного более многословен, чем использование литерала объекта, и наш пример настолько прост, что мы не видим каких-либо преимуществ от использования класса (хотя в последнем примере это пригодится).
Одно из преимуществ маршрута класса, которое может быть неочевидным, состоит в том, что, если это ваш интерфейсный код, а ваш серверный написан на C # или Java, вы можете использовать множество одинаковых шаблонов проектирования в своем клиентском приложении. как вы делаете на бэкэнде, и повышаете эффективность вашей команды (если вы маленький, а люди работают на полную ставку). Звучит мягко и трудно поддается измерению, но я сам испытал это, работая над приложением C # с React-интерфейсом, и выгода была реальной.
Следует отметить, что с технической точки зрения мотивированный провокатор может подорвать неизменность и неопределяемость синглтона с использованием обоих этих шаблонов. Литерал объекта может быть скопирован, даже если он сам является константой, используя Object.assign . И когда мы экспортируем экземпляр класса, хотя мы непосредственно не подвергаем сам класс потребляющему коду, конструктор любого экземпляра доступен в JavaScript и может быть вызван для создания новых экземпляров. Очевидно, однако, что все это требует, по крайней мере, небольшого усилия, и, надеюсь, ваши коллеги-разработчики не так настаивают на нарушении схемы синглтона.
Но, скажем, вы хотели быть уверены, что никто не напутал с единственностью вашего синглтона, и вы также хотели, чтобы он еще более точно соответствовал реализации синглетонов в объектно-ориентированном мире. Вот что вы можете сделать:
const
Добавив дополнительный этап удержания ссылки на экземпляр, мы можем проверить, class UserStore {
constructor(){
if(! UserStore.instance){
this._data = [];
UserStore.instance = this;
}
return UserStore.instance;
}
//rest is the same code as preceding example
}
const instance = new UserStore();
Object.freeze(instance);
export default instance;
UserStore
Как вы можете видеть, это также хорошо использует тот факт, что мы сделали UserStore
Мысли? Ненавижу почту?
Нет сомнений в том, что многие разработчики уже несколько лет используют старый шаблон singleton / module в JavaScript и считают, что он вполне им подходит. Тем не менее, поскольку поиск лучших способов сделать что-то является центральным в духе того, чтобы быть разработчиком, мы надеемся, что мы увидим более чистые и легкие для рассуждения шаблоны, подобные этому, приобретающие все большую популярность. Особенно, когда становится проще и более распространенным использовать функции ES6 +.
Это шаблон, который я использовал на производстве, чтобы построить магазины в пользовательской реализации Flux (магазины, в которых было немного больше, чем в наших примерах), и это сработало хорошо. Но если вы видите дыры в нем, пожалуйста, дайте мне знать. Также, пожалуйста, отстаивайте любой из новых шаблонов, который вы предпочитаете, и считаете ли вы, что объектные литералы являются подходящим способом, или если вы предпочитаете классы!