Ember — Лучшие практики: Избегайте утечки состояния внутрь фабрики

В DockYard, мы много времени уделяем Ember, от построения web приложений, создания и поддержки аддонов, до вклада в экосистему Ember. Мы надеемся поделиться некоторым опытом, который мы приобрели, через серию постов, которые будут посвящены лучшим практикам Ember, паттернам, антипаттернам и распространённым ошибкам. Это первый пост из данной серии, так что давайте начнём, вернувшись к основам Ember.Object.

Ember.Object это одна из первых вещей, которую мы узнаём, как разработчики Ember, и неудивительно. Практически каждый объект, с которым мы работаем в Ember, будь то маршрут (Route), компонент (Component), модель (Model), или сервис (Service), наследуется от Ember.Object. Но время от времени, я вижу как его неправильно используют:

export default Ember.Component.extend({   items: [],    actions: {     addItem(item) {       this.get('items').pushObject(item);     }   } });

Для тех, кто сталкивался с этим раньше, проблема очевидна.

Ember.Object

Если вы посмотрите API и отмените выбор всех Inherited (Наследуемых), Protected (Защищённых), и Private (Приватных) опций, вы увидите, что Ember.Object не имеет собственных методов и свойств. Исходный код не может быть короче. Это буквального расширение Ember CoreObject, с примесью Observable:

var EmberObject = CoreObject.extend(Observable);

CoreObject обеспечивает чистый интерфейс для определения фабрик или классов. Это, по существу, абстракция вокруг того, как вы обычно создаёте функцию конструктор, определяя методы и свойства на прототипе, а затем создавая новые объекты с помощью вызова new SomeConstructor(). За возможность вызывать методы суперкласса, используя this._super(), или объединять набор свойств в класс через примеси, вы должны благодарить CoreObject. Все методы, которые часто приходиться использовать с Ember objects, такие как init, create, extend, или reopen, определяются там же.

Observable это примесь (Mixin), которая позволяет наблюдать за изменениями свойств объекта, а также в момент вызова get и set.

При разработке Ember приложений, вам никогда не приходиться использовать CoreObject. Вместо этого вы наследуете Ember.Object. В конце концов, в Ember самое важное реакция на изменения, так что вам нужны методы с Observable для обнаружения изменения значений свойств.

Объявление нового класса

Вы можете определить новый тип наблюдаемого объекта путем расширения Ember.Object:

const Post = Ember.Object.extend({   title: 'Untitled',   author: 'Anonymous',    header: computed('title', 'author', function() {     const title = this.get('title');     const author = this.get('author');     return `"${title}" by ${author}`;   }) });

Новые объекты типа Post теперь могут быть созданы путём вызова Post.create(). Для каждой записи будут наследоваться свойства и методы, объявленные в классе Post:

const post = Post.create(); post.get('title'); // => 'Untitled' post.get('author'); // => 'Anonymous' post.get('header'); // => 'Untitled by Anonymous' post instanceof Post; // => true

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

post.set('title', 'Heads? Or Tails?'); post.set('author', 'R & R Lutece'); post.get('header'); // => '"Heads? Or Tails?" by R & R Lutece'  const anotherPost = Post.create(); anotherPost.get('title'); // => 'Untitled' anotherPost.get('author'); // => 'Anonymous' anotherPost.get('header'); // => 'Untitled by Anonymous'

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

Утечка состояния внутрь класса

Пост может иметь дополнительный список тегов, так что мы можем создать свойство с именем tags и по умолчанию это пустой массив. Новые теги могут быть добавлены при помощи вызова метода addTag().

const Post = Ember.Object.extend({   tags: [],    addTag(tag) {     this.get('tags').pushObject(tag);   } });  const post = Post.create(); post.get('tags'); // => [] post.addTag('constants'); post.addTag('variables'); post.get('tags'); // => ['constants', 'variables']

Похоже, что это работает! Но проверим, что происходит, после создания второго поста:

const anotherPost = Post.create(); anotherPost.get('tags'); // => ['constants', 'variables']

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

post.get('tags'); // => ['constants', 'variables'] anotherPost.get('tags'); // => ['constants', 'variables'] anotherPost.addTag('infinity'); // => ['constants', 'variables', 'infinity'] post.get('tags'); // => ['constants', 'variables', 'infinity']

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

const Post = Ember.Object.extend({   createdDate: new Date() });  const postA = Post.create(); postA.get('createdDate'); // => Fri Sep 18 2015 13:47:02 GMT-0400 (EDT)  // Sometime in the future... const postB = Post.create(); postB.get('createdDate'); // => Fri Sep 18 2015 13:47:02 GMT-0400 (EDT)

Как держать ситуацию под контролем?

Для того, чтобы избежать совместного использования меток между постами, свойство tags необходимо будет установить, во время инициализации объекта:

const Post = Ember.Object.extend({   init() {     this._super(...arguments);     this.tags = [];   } });

Так как init вызывается всякий раз во время вызова Post.create(), каждый пост экземпляра всегда получит свой собственный массив tags. Кроме того, можно сделать tags вычисляемым свойством (computed property):

const Post = Ember.Object.extend({   tags: computed({     return [];   }) });

Вывод

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

Эта ошибка может встречаться при использовании примесей. Несмотря на то, что Ember.Mixin это не Ember.Object, объявленные в нём свойста и методы, примешиваюся к Ember.Object. Результат будет тем же: вы можете в конечном итоге разделить состояние между всеми объектами, которые используют примесь.

ссылка на оригинал статьи https://habrahabr.ru/post/318096/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *