пятница, 27 апреля 2018 г.

Паттерн Singleton на JavaScript

В данной статье, мы погрузимся в самый правильный способ реализации паттерна синглтона на JavaScript, и посмотрим как она развивалась с появлением ES6.
Среди языков которые активно используются в настоящее время, JavaScript пожалуй является наиболее активно развивающимся, и с каждой новой спецификацией он уже больше похож на Python, чем на ранние версии самого себя. Несмотря на то что некоторые изменения нравятся не всем, новый JavaScript проще читать и как следствие проще писать его в понятном стиле (например с поддержкой принципов модулярности SOLID) и применять в коде различные виды известных паттернов.

Объяснение ES6

ES6 (также известный как ES2015) был первым крупным обновлением языка со времен стандарта ES5, который увидел свет в 2009 году. Почти все современные браузеры поддерживают ES6. Однако, если вам нужно использовать более старые браузеры, ES6 может быть достаточно просто предобразован в ES5 с помощью инструмента Babel. ES6 добавляет языку массу новых возможностей, включая замечательный синтаксис для классов, и новые ключевые слова для объявления переменных. Вы можете изучить подробнее эти вопросы по данной ссылке.

Что такое синглтон

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

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

Старый способ создания синглтона в JavaScript

Традиционный способ создания синглтона в JavaScript подразумевает использование замыканий и немедленное выполнение функции выражения. Вот пример того, как мог бы быть написать подобный код:
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 воовсе. Более того, модификация кода может произойти где угодно. Если в результате таких изменений мы получим ошибку, то найти ее в крупном проекте будет весьма непросто.

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

Новый способ

С помощью возможностей ES6, в нашем случае модулей и нового способа объявления константной переменной, мы может реализовать синглтон, который не только имеет более точную реализацию, но и лучше соответсвует нашим треботваниям.
Давайте начнем с самой простой реализации. Вот современная реализация того что мы видели ранее:
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 – преимущества были очевидны.
Стоит также отметить, что технически неизменность и невозможность перегрузить синглтон в обоих случаях может быть преднамерено нарушена. Объектный литерал можно скопировать, даже если он помечен как const, с помощью функции Object.assign. И когда мы экспортируем экземпляр класса, то мы как бы не расскрываем эго содержимое, но конструктор данного экземпляра доступный в JavaScript и может быть вызван для создания нового экземпляра. Очевидно, что для всего это необходимо приложить целенаправленные усилия, и скорее всего ваши разработчики не будут этим заниматься только что того что бы сломать синглтон.
 Но давайте скажем что вы хотите быть уверенным, что никто ни при какихъ условиях не повредит синглтон, и вы также хотите реализовать его в том виде как это принято в объектно-ориентированных языках. Вот что у нас может получиться:

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 стал классом, пользоваться им стало удобнее.

Мысли? злобные письма?

Нет вомнений в том, что многие разработчики, которые на протяжении многих лет использовали старую реализацию синглтона, считают хорошей и ее. Тем не менее очень важно постоянного развиваться как квалифицированный разработчик, и поэтому мы надеемся, что в будущем мы увидим использование более простых и эффективных реализаций паттерном, подобных этому. Особенно когда становится проще и привычнее использовать функции ES6+.
Этот паттерн, который я реализовал, используется в реализации хранилища кастомной реализации Flux (которая немного сложнее чем в приведенном примере выше), и она работает хорошо. Но если вы видите какие то недочеты в ней, пожалуйста сообщите. Также можете поделится релазациями других паттернов, которыми вы пользуетесь. Можете оставить свое мнение об использовании объектных паттернов и классов.