21 April 2015

Миксины для “классов” в JavaScript

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

Проблема в картинках


Начнем с визуализации нашей проблемы. Допустим у нас есть два базовых класса и от них наследуются два дочерних класса.



В какой-то момент в дочерних классах появляется необходимость в одинаковом функционале. Обычная копипаста будет выглядеть на нашей схеме вот так:



Очень часто бывает, что данный функционал не имеет ничего общего с родительскими классами, поэтому выносить его в какой-то базовый класс нелогично и неправильно. Вынесем его в отдельное место — миксин. С точки зрения языка миксин может быть обычным объектом.



А теперь обсудим момент, ради которого написана вся статья — как правильно замешивать наш миксин в классы.

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



Плюсы данного подхода


  • простота реализации;
  • легкость переопределения содержащегося в миксине кода;
  • гибкость подключения миксинов, возможность создания зависимых миксинов без особого труда;
  • использование еще одного паттерна в коде не усложняет его понимание и поддержку, потому что используется существующий механизм наследования;
  • скорость вмешивания — чтобы замешать миксин подобным образом не требуется ни единого цикла;
  • оптимальное использование памяти — вы не копируете ничего

Пишем код


Во всех последующих примерах будет использоваться конкретная реализация — библиотека Backbone.Mix. Посмотрев код, вы обнаружите, что он чрезвычайно прост, поэтому вы можете легко адаптировать его для своего любимого фреймворка.

Давайте посмотрим, как применять миксины, встраивающиеся в цепочку наследования, в реальной жизни и прочувствуем плюсы данного подхода на практике. Представьте, что вы пишете сайт )) и на вашем сайте есть разные штуки, которые можно закрывать — попапы, хинты и т.п. Все они должны слушать клик по элементу с CSS классом close и скрывать элемент. Миксин для этого может выглядеть так:

var Closable = {
   events: function () {
       return {
           'click .close': this._onClickClose
       };
   },

   _onClickClose: function () {
       this.$el.hide();
   }
};

Вмешиваемся!!!


var Popup = Backbone.View.mix(Closable).extend({
   // что-то невероятное здесь
});

Довольно просто, не правда ли? Теперь наша цепочка наследования выглядит так:



  • сначала идет базовый класс Backbone.View
  • от него наследуется анонимный класс, прототипом которого является миксин Closable
  • завершает цепочку наш Popup

Такая схема позволяет очень легко переопределять и доопределять методы из миксина в классе, к которому он примешан. Например, можно сделать чтобы Popup при закрытии писал что-нибудь в консоль:

var Popup = Backbone.View.mix(Closable).extend({
   _onClickClose: function () {
       this._super();
       console.log('Popup closed');
   }
});

Здесь и далее в примерах используется библиотека backbone-super

Примеси, которые не мешают..


… а помогают. Бывает замес выходит не хилый, и одним миксином не обойтись. Например, представьте что мы — крутые пацаны, и пишем лог в IndexedDB, а еще у нас для этого свой миксин — Loggable :)

var Loggable = {
   _log: function () {
       // пишем в IndexedDB
   }
};

Тогда к попапу мы будем мешать уже два миксина:

var Popup = Backbone.View.mix(Closable, Loggable).extend({
   _onClickClose: function () {
       this._super();
       this._log('Popup closed');
   }
});

Синтаксис вроде не сложный. На схеме это будет выглядеть так:



Как видите, цепочка наследования выстроится в зависимости от порядка подключения миксинов.

Зависимые миксины


А теперь представьте ситуацию, что к нам подходит наш аналитик и сообщает, что хочет собирать статистику по всем закрытиям попапов, хинтов — всего, что может закрываться. Конечно же, у нас давно есть миксин Trackable для таких случаев, с того времени, как мы делали регистрацию на сайте.

var Trackable = {
   _track: function (event) {
       // отсылаем событие в какую-нибудь систему сбора аналитики
   }
};

Немудрено, что мы хотим связать Trackable и Closable, а точнее Closable должен зависеть от Trackable. На нашей схеме это будет выглядеть так:



И в цепочке наследования Trackable должен оказаться раньше, чем Closable:



Код для миксинов с зависимостями немного усложнится:

var Closable = new Mixin({
   dependencies: [Trackable]
}, {
   events: function () {
       return {
           'click .close': this._onClickClose
       };
   },

   _onClickClose: function () {
       this.$el.hide();
       this._track('something closed'); // <- появившаяся функциональность
   }
});

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

var Popup = Backbone.View.mix(Closable, Loggable).extend({ … });

Документируй миксины правильно


В WebStorm есть прекрасная поддержка миксинов. Достаточно лишь правильно писать JSDoc, и подсказки, автокомплит, понимание средой общей структуры кода заметно улучшится. Среда понимает тэги @mixin и @mixes. Посмотрим на пример задокументированных миксина Closable и класса Popup.

/**
* @mixin Closable
* @mixes Trackable
* @extends Backbone.View
*/
var Closable = new Mixin({
   dependencies: [Trackable]
}, /**@lends Closable*/{
   /**
    * @returns {object.<function(this: Closable, e: jQuery.Event)>}
    */
   events: function () {
       return {
           'click .close': this._onClickClose
       };
   },

   /**
    * @protected
    */
   _onClickClose: function () {
       this.$el.hide();
       this._track('something closed');
   }
});

/**
* @class Popup
* @extends Backbone.View
* @mixes Closable
* @mixes Loggable
*/
var Popup = Backbone.View.mix(Closable, Loggable).extend({
/**
* @protected
*/
_onClickClose: function () {
       this._super();
       this._log('Popup closed');
   }
});

Очень часто миксин пишется для классов, имеющих определенного предка. Наш Closable, написанный для классов, унаследованных от Backbone.View — отнюдь не исключение. В такой ситуации среда не поймет, откуда в коде миксина встречаются вызовы методов данного предка, если ей явно не указать @extends:

/**
* @mixin Closable
* @mixes Trackable
* @extends Backbone.View
*/
var Closable = new Mixin(...);

На этом, пожалуй всё, счастливого вмешивания!

Англоязычная версия в моем блоге
Библиотека Backbone.Mix
Еще код от тех же авторов: backbonex
Что делать с jQuery лапшой, чтобы привести ее к виду, когда можно задуматься о миксинах? In english Сразу код
Мой твиттер (только про код)
Tags:mixinjavascriptbackbonejsdocoop patterns
Hubs: JavaScript
+11
20.8k 114
Comments 4