Проблема в картинках
Начнем с визуализации нашей проблемы. Допустим у нас есть два базовых класса и от них наследуются два дочерних класса.
В какой-то момент в дочерних классах появляется необходимость в одинаковом функционале. Обычная копипаста будет выглядеть на нашей схеме вот так:
Очень часто бывает, что данный функционал не имеет ничего общего с родительскими классами, поэтому выносить его в какой-то базовый класс нелогично и неправильно. Вынесем его в отдельное место — миксин. С точки зрения языка миксин может быть обычным объектом.
А теперь обсудим момент, ради которого написана вся статья — как правильно замешивать наш миксин в классы.
Исходя из собственного опыта, могу заявить, что самый удобный способ — это создание временного класса на основе миксина и подстановка его в очередь наследования.
Плюсы данного подхода
- простота реализации;
- легкость переопределения содержащегося в миксине кода;
- гибкость подключения миксинов, возможность создания зависимых миксинов без особого труда;
- использование еще одного паттерна в коде не усложняет его понимание и поддержку, потому что используется существующий механизм наследования;
- скорость вмешивания — чтобы замешать миксин подобным образом не требуется ни единого цикла;
- оптимальное использование памяти — вы не копируете ничего
Пишем код
Во всех последующих примерах будет использоваться конкретная реализация — библиотека 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 Сразу код
Мой твиттер (только про код)
Комментарии (4)
friday
21.04.2015 18:06А зачем делать из миксинов цепочку? Чем плохо, например, смешивание всех миксинов в один промежуточный «класс» или вообще подмешивание их напрямую в прототип? Результат почти тот же (да, придётся пробежаться циклом, но это нужно сделать только один раз при инициализации), но цепочка прототипов будет короче, что влияет на скорость поиска и вызова методов.
asavin
21.04.2015 22:19Не всегда возможно смешать все миксины в один класс или напрямую в прототип. Например, если в цепочке А — миксин Х — миксин У — Б в миксине У доопределяется метод из миксина Х.
Ну и не стоит сильно переживать за длину цепочки, в реальных приложениях тормозить будут совсем другие вещи.
Ilusha
21.04.2015 19:45Цепочка миксинов это жесть.
Понятно, что такое решение появилось не просто так, но все же.
Начал использовать react.js с fluxxor, где нет классического наследования.
За пару месяцев втянулся, но миксины меня, порой, добивают.
Основные проблемы/неудобства, навскидку:
- поиск незнакомого миксина и разбор его работы;
- ide часто не понимает, что метод берется из миксина;
- говнокод, который писался под @todo исправить это позже
- часто миксины берут на себя большую функциональность, чем должны: по сути нужно уже выделять в отдельный класс, но это ведет уже к рефакторингу и очередной попоболи.
Понятно, что при грамотном подходе можно все решить, но, на мой взгляд, миксины усложняют код.
Инструмент интересный, в некоторых случаях — необходимый, но стоит к нему относиться осторожно.
Слишком уж легко выстрелить себе в ногу.
k12th
Если миксин тупой и является просто набором методов, которые вы просто дергаете сами вручную, то можно даже не городить огород, а тупо вынести их в отдельный модуль и точно так же дергать в нужном контексте.
А если миксину нужна инициализация или еще какое-то участие в жизненном цикле объекта, в который мы его вмикшиваем, да еще если они наследуются друг от друга, то получаем, по факту, все прелести и проблемы множественного наследования. Чтобы этого избежать, лучше взять компоненты.