суббота, 8 октября 2011 г.

Обогащение (enrichment) или псевдо-конструктор (pseudo-constructor) как JavaScript-pattern

Вторым открытым при разработке библеотеки Drag and Drop приёмом, который я бы назвал шаблоном, является обогащение (enrichment). Ещё я иногда про себя называю его псевдо-конструктор (pseudo-constructor), что считаю менее удачным, но так же подходящим для него названием.

Он служит для ситуаций, когда нужно придать некоторую дополнительную функциональность группе объектов одинакового вида.

Я столкнулся с этой проблемой при работе с тем же самым объектом Event для IE. Дело в том, что продемонстрированный в позапрошлой статье приём "Заплатка" (Patch) был несколько неполным. Напомню приведённый код:

(function setAddEventListener(){
    if ('attachEvent' in this && !('addEventListener' in this))
        addEventListener = function(eventName, handler, isCapturing) {
            if (isCapturing)
                throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");
            attachEvent("on" + eventName, handler);
    }
})(); //IE patch for window.addEventListener
В этом коде, к сожалению, не было учтено, что для придания стандартного поведения функции addEventListener нужно, что бы она вызывала слушателя, передавая ему объект Event, при чём это должен быть объект Event стандартного вида.

Но в случае, если выполнение сценария происходит в IE, у нас есть только IE`шный Event, который отличается от стандартного достаточно сильно. Так что не достаточно просто подставить при вызове его - нужно обязательно придать ему нужную функциональность.

Имитация функциональности стандартного объекта Event на почве IE`шной реализации сама по себе является сложной задачей и у меня нет уверенности, что она действительно представляет для читателей практический интерес, так что я просто проиллюстрирую принцип, показав скелет решения, а уже "наполнение мясом", каждый может делать для себя сам - по крайней мере для меня эта задача пока не актуальна. Так что упростим задачу - возьмём лишь один аспект нестандартного поведения и его сэмулируем. Например, отмена поведения браузера по-умолчанию для данного события. В стандартном W3C`шном объекте Event это действие производится вызовом метода preventDefault, а в IE - установкой значения false в свойство returnValue этого кода.

Итак, имеем нестандартный Event, который нужно сделать стандартным Event`ом. Для выбранного нами аспекта это означает, что ему нужно приделать такую функцию:

/** Отменяет поведение браузера для данного события по-умолчанию. */
event.preventDefault = function(){
    this.returnValue = false;
};
Это нельзя сделать раз и навсегда, поскольку сам объект постоянно (при наступления каждого нового события) меняется. Тут мы, кстати, видимо, что в основу этой модели уже заложена однопоточность выполнения JavaScript-сценариев, которую сейчас так активно критикует JavaScript-сообщество. Так же однопоточность заложена в основу на этот раз стандартного конструктора RegExp. Первое, что приходит на ум, это просто приписать этому объекту несколько новых свойств "не отходя от кассы" - т.е. там, где это нам понадобится. Однако это будет иная логика, которая тем самым замусорит код - будет удобнее собрать все эти операции в одном, отдельном месте.

Следующая мысль, уже более зрелая - создать отдельный конструктор, который наследовался бы от нестандартного Event`а и расширял бы его функционал стандартными свойствами и методами:

/** Конструктор, служащий для придания не совместимым с W3C DOM level 3 объектам
 * Event стандартного поведения.
 * @constructor
 * @extends Event */
function EventW3C() {
    /** Отменяет поведение браузера для данного события по-умолчанию. */
    this.preventDefault = function() {
        this.returnValue = false;
    };
};
Вызов же этого конструктора в нужном месте предполагается примерно такой:
EventW3C.prototype = event; //Сначала присваиваем правильный прототип конструктору
var w3cEvent = new EventW3C(); //Затем создаём экземпляр, который будет ссылаться ссылкой __proto__ куда надо.
После этого получим объект, поведение которого соответствует стандартному и который уже можно спокойно передавать функции, не подозревающей, что она выполняется в нестандартном (IE6-8) браузере.

Однако нетрудно увидеть, что этот приём излишне-расточителен. Фактически, для каждого события он создаёт не один, а два объекта в памяти, так что сборщику мусора будет в два раза больше работы. Кроме того, само по себе создание объектов - не такая уж быстрая операция и хотелось бы по возможности её избегать, если это не сильно оправданно ситуацией. Не говоря уже о том, что механизм вызова из двух выражений выглядит излишне-громоздким.

Гораздо более экономным является такой вариант - можно вызвать эту функцию не при помощи оператора new, а при помощи метода call:

EventW3C.call(event);
и объект сам, без создания наследника, получит нужное свойство. Именно из-за того, что по форме это очень напоминает использование конструктора, я про себя иногда и называю этот шаблон псевдо-конструктором (pseudo-constructor).

Как нетрудно видеть, этот шаблон по сути является использованием реального конструктора в качестве обогащающей объект новыми свойствами функции. При этом сам по себе конструктор продолжает выполнять функцию конструктора, так что, наверное, можно придумать элегантный случай, в котором эта функция будет использована и как конструктор и как псевдо-конструктор. Единственное, что для большего удобства использования можно добавить необязательное выражение внутрь этого конструктора, а именно в его конец:

function EventW3C() {
    /** Отменяет поведение браузера для данного события по-умолчанию. */
    this.preventDefault = function() {
        this.returnValue = false;
    };
    return this;
};
Для использования этой функции в качестве реального конструктора это ровным счётом ничего не изменит, а вот для использования её в качестве псевдо-конструктора это может сделать вызов более удобным - тогда само выражение вызова будет возвращать результат, прямо как у реального конструктора:
var w3cEvent = EventW3C.call(event);
На последок приведу полный вариант реализации pattern`а "Заплатка" ("Patch") для эмуляции addEventListener`а, который использует приведённый pattern Обогащение (Enrichment) или псевдо-конструктор (Pseudo-Constructor):
//IE patch for window.addEventListener
function setAddEventListener()
{
    if ('attachEvent' in this && !('addEventListener' in this))
        addEventListener = function(eventName, handler) {
            if (isCapturing)
                throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");
            attachEvent("on" + eventName, function() {
                handler(EventW3C.call(event) //Здесь используется pattern "Enrichment"
            });
        }
}
setAddEventListener.call(document);// Применяем заплатку для объекта document
Конечно, полученные применением этого шаблона объекты не являются в строгом смысле потомками этого конструктора, о чём не применит сообщить операция instanceof, но для практического применения это чаще всего не нужно, в то время как с полученными в результате этого объектами фактически можно обращаться как с наследниками. Так что для с одной стороны - экономии ресурсов, а с другой - удобства написания красивого и лаконичного кода без засорения логики, этот шаблон, на мой взгляд, прекрасно подходит.

P.S. Думается, что данный шаблон имеет так же большой потенциал применения для добавления свойств объектам DOM, если нужно придать им нужную функциональность. По крайней мере сейчас я прорабатываю эту идею как раз в контексте своей библиотеки - она поможет пользователям навешивать несколько обработчиков событий в ходе Drag&Drop-перемещения.

P.P.S. Пришло в голову, когда уже собирался опубликовать сообщение - а может, лучше будет назвать этот приём - Псевдо-наследование ("Pseudo-Inheritance")? Пишите в комментах Ваши версии :)

Комментариев нет: