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

от автора

Одинаковый код в нескольких местах — это боль. Сегодня я напишу пару слов про повторяющиеся куски классов. Люди давно придумали решение — можно вынести одинаковые методы и свойства в общий базовый класс, а если такового нет — использовать примеси. Существует миллион реализаций данного паттерна для 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 Сразу код
Мой твиттер (только про код)

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


Комментарии

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

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