среда, 2 марта 2016 г.

Правильная ориентация на клиента. Часть №1: Плохому парикмахеру – клиент мешает…

Парикмахер
- Сколько будет дважды два?
- А сколько Вам надо?..

Анекдот.

В последнее время меня всё больше угнетает, когда в IT говорят о том, что самое главное – это удовлетворить заказчика, сделав всё так, как ему хочется. Мол, не важно, как правильно – надо делать так, как он хочет. Да простят меня менеджеры, заказчики и прочее начальство, но я, с высоты своего не такого уж и маленького опыта в этой сфере, так действительно не считаю. И прежде чем сжечь меня на костре, как вреднейшего еретика, покусившегося на «священную корову» заказной разработки, прошу дать мне возможность высказать последнее слово.

Интуитивно, я уверен, каждый чувствует, что есть что-то недостойное в том, чтобы просто делать так, как сказал клиент. И дело не в профессиональной гордости, не в амбициях, а просто в том, что это по сути часто является профанацией решения задачи, эдакой «итальянской забастовкой». Это часто даже граничит с прямым обманом – знали бы Вы, какие ужасные решения иногда одобряет заказчик!..

Поясню на примере.

Достаточно часто мне, как и многим читателям, я уверен, приходится ходить в парикмахерскую стричься. И, часто обращаясь к различным парикмахерам, я выявил некоторое правило, которому стараюсь следовать. Правило такое – никогда не отвечать по существу на предварительные вопросы. Дело в том, что я – не парикмахер, чтобы управлять процессом своей стрижки императивно, и даже не стилист, чтобы управлять процессом стрижки декларативно, вкуса у меня нет, поэтому я не хочу никак этим управлять вообще. Я могу лишь сказать по факту – нравится мне как меня постригли или нет, но никак сформулировать, что мне понравится, до того, как меня постригут, я не могу. Да и даже с моими «нравится – не нравится» часто бывают засады – сначала нравится, а потом – «не очень...», или даже «очень не…». А ещё бывает так - иногда нравящаяся мне стрижка потом другим людям не нравится, что не нравится уже мне. Т.е. я не знаю, как мне будет хорошо. Не-зна-ю! Я лишь знаю, что волосы отросли и их надо постричь так, чтобы было красиво, а конкретизировать понятие «красиво» по отношению к стрижке – не могу. И я совсем не хочу об этом париться – у меня своих забот хватает – надо разбираться с проектными задачами, с технологиями, инструментами для программирования, которые чуть ли не каждый день появляются новые... Просто, глядя на себя потом в зеркало, мне не хочется испытывать дискомфорт, да и не приятно будет, если кто-нибудь из родных и друзей скажет что-нибудь нелестное о моей стрижке.

Так что, если какой-то парикмахер, когда я усядусь в кресло, принимается интенсивно расспрашивать меня – сколько мне тут подстричь, сколько там, а вот тут как сделать? – я отвечаю односложно – «На ваш вкус», «сделайте так, как вам кажется лучше», «так, как будет красиво» - и всё. Если парикмахер упорствует в своих уточнениях – я встаю и ухожу. Потому что опытным путём уже давно установил, что все эти расспросы, если я пытаюсь на них ответить, заканчиваются всегда одним – стригут меня плохо. Конкретизирую я лишь два момента – виски косые и в какую сторону пробор. Ну и в целом выражаю пожелание, что бы постригли покороче (что бы подольше не пришлось снова идти стричься). И на этом все мои возможности по конкретизации требований к стрижке заканчиваются.

Хороший парикмахер отличается тем, что он тебя больше ни о чём и не спросит. Он сделает всё сам, возьмёт на себя риск, что получится не красиво, поскольку уверен в своих силах. У меня даже есть подозрение, что плохой парикмахер, предвидя, что пострижёт меня плохо, устраивает эти расспросы, чтобы потом была возможность оправдаться – мол, я сам ему сказал, что хочу именно так, вот он меня именно так и постриг, какой тогда с него спрос теперь?.. Т.е. расспросы – это, возможно, вообще разводка…

Вот и тут – плох тот аналитик, который обсуждает детали требований с заказчиком. Хорош тот аналитик, который обсуждает с клиентом детали его бизнеса и из понимания этого сам выявляет необходимые требования.

Продолжение следует...

четверг, 22 января 2015 г.

Принцип "Прямой совместимости" и Web-интерфейсы

Принципом "Прямой совместимости" я называю антипод всем известного принципа "обратной совместимости" (отсюда и название). Заключается он в том, что бы, когда ты вводишь какую-то технологическую инновацию на рынок в новой версии своего продукта, делать предыдущие решения так же с ней совместимыми. В качестве примера можно рассмотреть Microsoft - когда они в 2007-м Office вводили новый формат файлов "Open XML" с приставкой "x" на конце ("*.docx", "*.xslx" и т.д.), они (удивительное дело!) позаботились о том, что бы пользователи 2003-й версии MS Office`а могли работать с этим форматом, выпустив для этой версии соответствующий patch. Так же я ещё помню, как тогда ещё Sun Microsystems, вводя в язык Java Generic`и лишь в 5-й версии, ещё до её final-релиза выпустила update-pack на Java SE 1.4 SDK, который по сути добавлял поддержку Generic`ов в 4-ю версию. Это, что называется, "на слуху", но, думаю, если читатели поразмыслят, то вспомнят и другие примеры такого рода решений прямой совместимости в IT-индустрии.

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

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

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

Давайте представим, например, транслятор, который преобразовывал бы код на Java SE 8 в код на Java SE 7. Лямбды бы, очевидно, превращались в анонимные классы. Отсутствующие классы стандартных библиотек подтягивались бы в качестве библиотек внешних, для этого заюзали бы Jigsaw (на какой она там альфа-фазе, правда, не знаю) - упаковать их в jar`ники, вставить в проект при сборке - и вся недолга! Stream`ы преобразуются в Fork/Join пулы (поверх которых и реализованы). Time API вообще в-лёгкую переписывается на генетически родственную библиотеку Joda Time. Всякие дефолтные и статические методы интерфейсов как-то разруливались бы через абстрактные классы, которые бы генерились и вставлялись в цепочки наследования. Nashorn преобразовывался бы в так же подтягивающийся Rhino - и т.д. Если бы мы грамотно разрулили все такого рода нюансы, то, в идеале, получали бы на выходе вполне выполняющийся код под JRE 1.7. Может быть даже комментарии бы умудрились правильно сохранять, что бы JavaDoc`и, например, генерились корректно. Может быть (а чего бы не помечтать?), даже смогли бы написать не один, а два транслятора - и туда и обратно, протащив не только логику Java 8 в 7`ку, но и обратно, тогда получили бы возможность дать разработчикам самим выбирать - на Java 7 или на Java 8 им писать и часть большого проекта могли бы писать на Java 7, а часть - на Java 8. И дебажить могли бы спокойно, потому что дебаговая инфа бы сохранялась (через какие-нибудь хитрые аннотации, например).

Теперь давайте подумаем - если бы мы добросовестно такой проект реализовали и начали бы на нём работать - имело бы смысл заказчику переходить на новую версию Java? Менять 7`ку на 8`ку?

Мы, ведь, знаем, что лямбды, к примеру, отличаются от анонимных классов не только по форме - на уровне синтаксиса, но и на уровне байт-кода! И, очевидно, эти отличия значимы в первую очередь для производительности. Т.е. реально это - не синтаксический сахар, в то время, как такое вот наше решение могло бы остаться лишь исключительно на уровне синтаксического сахара. Да и Joda Time, боюсь, не так быстра, как Time API из Java 8 - всё-таки тюнить свой код (по крайней мере под стандартную JVM) Oracle`исты умеют лучше OpenSource`ников. Не говоря уже о сравнительных тестах производительности Nashorn`а и Rhino...

Т.е. по факту, перейдя на написание кода на Java SE 8 и используя при компиляции описанную трансляцию кода, результирующий Java 7 байт-код нашего проекта просто выполнялся бы медленнее, кушал бы больше ресурсов памяти и процессорного времени - и т.д., т.е. мы бы такой реализацией добились бы выполнения функциональных, но, скорее всего, никак не смогли бы добиться выполнения нефункциональных требований. И тогда нам осталось бы заказчику постепенно капать на мозги, что бы он, если хочет, что бы софт выполнялся быстрее, переходил бы на новую версию, а нам, разработчикам, это было бы фиолетово - мы полюбому все плюшки удобства разработки и поддержки кода от Java 8 получили бы.

И заказчик через некоторое время перешёл бы и наш доблестный транслятор мы бы, ко всеобщему облегчению, выпилили бы и зажили счастливо.

С другой стороны, как ни удивительно, часто это может оказаться даже легче, чем реализовывать новую технологическую версию. В качестве примера здесь приведу приём, который я описывал некоторое время назад, который стал возможен благодаря интересному явлению гомоиконности, присущему языку Lisp (соответственно, и Clojure`у), а так же различным XML-технологиям, например, XSLT. Там я показывал, как можно составить таблицу преобразований на XSLT 1.0, которая будет преобразовывать таблицу преобразований XSLT 2.0 в таблицу преобразований XSLT 1.0 - и тогда сложная библиотека, реализующая XSLT 2.0 вам может оказаться не нужна...

С третьей стороны, внесению инноваций может сопротивляться, как это ни удивительно, сам спрос, для удовлетворения которого эти технологии как бы и разрабатываются! Например, какую ситуацию мы видим в мире Web-разработки? Есть довольно удобный драфт стандарта ECMAScript 6. Там, где нет балласта в виде огромного кол-ва пользователей с устаревшими браузерами, например в сообществе программистов под Node.js, разработчики давно уже используют все фишки этого неутверждённого стандарта и в ус, что называется, не дуют. Но когда заходит речь о браузерах, то всем становится грустно. Яков Файн, у которого я недавно брал интервью для журнала "Хакер" (текст можно почитать тут), например, оценивает темп появления поддержки новой версии стандарта у значительного кол-ва пользователей (достаточного для того, что бы забить на всех остальных) минимум в 2 года после его официального утверждения (при том, что официально он ещё даже не утверждён).
Конечно, не все производители официальных браузеров сидят, сложа руки - многие уже реализуют этот стандарт, но частично. Мониторить ситуацию в real-time можно тут: ECMAScript 6 compatibility table (кстати, как ни удивительно, но "вечный тормоз" на наших глазах стремительно превращается в "вечный газ" - на состояние сейчас флагманом внесения изменений с 72-мя процентами поддержки фич ES6 является Tech Preview IE 12-го, обогнав даже ночные сборки FF, правда, что бы увидеть это чудо, придётся ставить 10-ю версию Windows, так что приходится верить ребятам на слово).

Так же можно мониторить и поддержку новых версий HTML и CSS, например, тут: HTML5 & CSS3 Web Design.

Тут проблему для внесения изменений представляют как раз-таки старые версии браузеров, которые не спешат обновляться. И вот для них такой транслятор был бы очень в тему. И такие попытки действительно есть - это проекты типа ES6-shim, EJS, Traceur`а (кстати, удобно интегрированного в IDEA`ю), но, как легко видеть всё в той же табличке, все они так же, как и браузеры, страдают лишь частичной реализацией ES6. Да и комментарии, представляющие исключительную ценность для сжатия Google Closure Compiler`ом, они так же не сохраняют. Да и парсеры, например, Esprima, почти не поддерживают ES6. И даже IDEA 14`я, хоть и подсвечивает синтаксис удобных конструкций ES6, но пока не работает с этим языком в-серьёз - например, как правило ничего не подставляет по нажатию Ctrl+Spase.

А что Вам делать, если Вы терпите издержки от того, что Вам приходится до сих пор поддерживать IE 8, к примеру? И ладно, когда Вы работаете с корпоративным заказчиком, который сам будет платить за неспособность пользователей перейти на новую версию браузера, а если у Вас - Internet-проект c хитрым Web-interface`ом и Вы понимаете, что разрабатывать намного быстрее и поддерживать дешевле его было бы на ES6?..

Как хорошо было бы, если бы разработчики стандарта сами написали бы такой вот транслятор, как Sun, когда-то написавшая патч с Generic`ами для Java SDK 1.4, и она стала бы своего рода "эталонной реализацией" такого транслятора? Тогда разработчики в дизайн-студиях смогли бы быстро и легко перейти на новую версию языка, а создатели браузеров бы, поддерживая новый синтаксис языка, конкурировали бы между собой по нефункциональным требованиям. Правда, тогда нужно было бы делать проверку на стороне сервера - с какого браузера к нам зашли? Есть ли там этого и этого поддержка? Нету? Тогда вставляем вот этот полифил и/или переформатируем JS-код. Есть? Тогда вот этот кусок JavaScript`а отдаём как есть. Сделать Плагин для Apache, к примеру - было бы вполне достаточно, благо его как прокси-кеш используют многие специализированные решения.

И мы проснулись бы в новом мире, в котором внесение инноваций не зависит от заказчиков или от рынка! И никто бы особо не пострадал, а выиграли бы потенциально все. Просто каждый выход того или иного изменения в той или иной технологии сопровождался бы вот таким вот типовым патчем, преобразующим "всё новое" в "хорошо забытое старое".

В общем, по крайней мере в некоторых случаях, как мне кажется, решения, сделанные как реализация принципа "Прямой совместимости", были бы более или менее полезны всем участникам рынка и привели бы к ускорению внесения инноваций.

пятница, 4 октября 2013 г.

Создание собственных событий в JavaScript

Иногда требуется организовать бизнес-логику, основываясь на событийном механизме (шаблон Observer). Обычно это делается разово, но последнее время такие ситуации стали появляться всё чаще, и я решил создать универсальное решение, которое позволит расширять или создавать список событий для любого JavaScript-объекта.

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

//Сначала добавляем нужные методы к объекту
var x = {};
setAddEvent(x);

//Затем добавляем событие. Называем его 'event1'. На выходе получаем функцию, которую нужно будет вызывать, когда событие произойдёт
var f = x.addEvent('event1');

// Теперь добавляем обработчики события - способ стандартный (такой же, как принятый в DOM)
var y = addEventListener('event1', function(){ console.log('Событие 1 произошло!'); }),
    z = addEventListener('event1', function(){ console.log('Событие 1 произошло! (Второй обработчик)'); }, false);

// Вызываем событие
f(); // "Событие 1 произошло!" и "Событие 1 произошло! (Второй обработчик)"

// Удаляем один из обработчиков
removeEventListener('event1', y, false)

// Снова вызываем событие
f(); // "Событие 1 произошло! (Второй обработчик)"

// Удаляем последний обработчик
removeEventListener('event1', z, false);

// Вызываем событие
f(); // Ничего не выводит

// Удаляем событие
removeEvent('event1');

// Пробуем вызвать событие
try { f(); } catch(/** @type Error */ e) { console.error(e.message); } // Выведет в консоль сообщение об ошибке: "Событие 'event1' было удалено!"
Примерно так. Добавлю ещё, что функция removeEventListener возвращает значение типа boolean, означающее наличие, либо отсутствие иных обработчиков данного события в настоящий момент.

Без лишних предисловий, вот основной код функции:
/** Функция делает переданный ей объект Observer`ом, т.е. генератором событий, на которые можно подписываться и
 * отписываться. После её вызова у переданного ей объекта появляются 3 метода: <code>addEvent(name)</code> для
 * добавления событий и пара функций <code>{@link #addEventListener}</code> и
 * <code>{@link #removeEventListener}</code> для добавления и удаления обработчиков событий.
 * @param {Object} that любой объект JavaScript, который нужно сделать Observer`ом.
 * @returns {Object} переданный в качестве аргумента, обогащённый методами <code>{@link #addEvent}</code>,
 * <code>{@link #addEventListener}</code> и <code>{@link #removeEventListener}</code> объект
 * <code><em>that</em></code>. */
function setAddEvent(that) {

    var /** @type Object<Object<Function[]>>*/ events = {};

    /** Цепляет к объекту событие, на которое после этого можно будет вешать обработчики при помощи метода
     * <code>{@link #addEventListener}</code>.
     * @param {string[]} name имя события (которое потом нужно будет передавать первым параметром - type - в метод
     * <code>addEventListener</code>).
     * @returns function(string, boolean) функция, которую нужно вызывать в ответ на событие, передавая ей при
     * вызове объект события. Функция будет вызывать все слушатели по порядку, передавая им этот объект. */
    that.addEvent = function(name) {

        if (name in events)
            throw new Error('Cобытие ' + name + ' уже есть!');

        /** @type Function[] */ (
            /** @type Object<Function[]> */ events[name] = {}
        )[true] = [];
        /** @type Function[] */ events[name][false] = [];

        return function(event, isPropagation) {
            if (!(name in events)) throw new Error('Событие \'' + name + '\' было удалено!');
            if (typeof isPropagation === 'undefined') isPropagation = false;
            for (var /** @type Function[] */ listeners = events[name][isPropagation],
                     /** @type number     */ i = 0,
                     /** @type Function   */ listener;
                 listener = listeners[i++];)
                listener(event);
        };
    };

    /** Удаляет событие и всех его слушателей из объекта.
     * @param {string} name Имя события, назначенное ему ранее при вызове метода <code>{@link #addEvent}</code>.*/
    that.removeEvent = function(name) {
        if (!(name in events))
            throw new Error(
                'События ' + name + ' уже итак нет, либо оно не было создано при помощи метода \'addEvent\'!');

        delete events[name];
    };

    /** Добавляет к объекту слушателя события, ранее созданного при помощи вызова метода
     * <code>{@link #addEvent}</code>. Если метод <code>addEventListener</code> у объекта уже есть, данный метод
     * выступает в качестве его Proxy, сохраняя его в замыкании и вызывая его для событий, которые не были созданы
     * при помощи вызова метода <code>{@link #addEvent}</code>.
     * @param {string} eventName Имя события объекта
     * @param {function(Event)} handler Обработчик события
     * @param {boolean} [isPropagation=false]
     * @returns {function(Event)} Параметр, переданный функции вторым - handler. */
    that.addEventListener = function(realSubject) {
        return function(eventName, handler, isPropagation){
            if (typeof isPropagation === 'undefined') isPropagation = false;
            if (eventName in events)
                events[eventName][isPropagation].push(handler);
            else
            if (typeof realSubject === 'function')
                realSubject.apply(this, arguments);

            return handler;
        };
    }(that.addEventListener);

    /** Удаляет из объекта обработчик события, ранее созданного при помощи вызова метода
     * <code>{@link #addEvent}</code>. Если метод <code>removeEventListener</code> у объекта уже есть, данный метод
     * выступает в качестве его Proxy, сохраняя его в замыкании и вызывая его для событий, которые не были созданы
     * при помощи вызова метода <code>{@link #addEvent}</code>.
     * @param {string} eventName Имя события объекта
     * @param {function(Event)} handler Обработчик события
     * @param {boolean} [isPropagation=false]
     * @returns {boolean} Остались ли ещё обработчики для данного события? Возврат значения <code>false</code>
     * указывает на бессмысленность дальнейшего вызова функции, возвращённой методом <code>{@link #addEvent}</code>
     * при создании данного события - пока при помощи метода <code>{@link #addEventListener}</code> не будет
     * прикреплён хотя бы один новый обработчик. */
    that.removeEventListener = function(realSubject) {
        return function(eventName, handler, isPropagation){
            if (typeof isPropagation === 'undefined') isPropagation = false;
            var /** @type Function[] */ listeners,
                /** @type number     */ index;
            if (eventName in events &&
                (index = (listeners = events[eventName][isPropagation]).indexOf(handler)) !== -1)
                listeners.splice(index, 1);
            else
            if (typeof realSubject === 'function')
                realSubject.apply(this, arguments);

            return listeners.length > 0 || events[eventName][!isPropagation].length > 0;
        };
    }(that.removeEventListener);

    return that;
}

//Test
var f = setAddEvent(this).addEvent('event1'),
    y = addEventListener('event1', function(){ console.log('Событие 1 произошло!'); }),
    z = addEventListener('event1', function(){ console.log('Событие 1 произошло! (Второй обработчик)'); }, false);
f();
addEventListener('load', function(){ console.log('loaded'); }, false);
console.log( 'Остались ли ещё обработчики? - ' + (
    removeEventListener('event1', y, false)
        ? 'Да': 'Нет'
    ));
f();
console.log( 'Остались ли ещё обработчики? - ' + (
    removeEventListener('event1', z, false)
        ? 'Да': 'Нет'
    ));
f();
removeEvent('event1');
try { f(); } catch(/** @type Error */ e) { console.error(e.message); }

среда, 10 июля 2013 г.

XSLT 2.0 -> 1.0

Известна проблема, связанная с реализацией XSLT 2.0 (и, соответственно, XPath 2.0) - как признаёт Дуг Тидуэлл (автор бестселлера по этой теме), фактически полноценных реализации всего две:
  • Altova
  • Saxon
- и обе платные. Все остальные - частичные. Что-что? Кто сказал, что Saxon - это типа OpenSource и, значит, по определению бесплатный? Платной является именно полноценная версия, в которой можно использовать Schema-типы данных. Бесплатные же реализации для XSLT есть только для версии 1.0 . При чём последних - как собак нерезаных! Они даже включены в наши с Вами браузеры. Действительно, можете это проверить - зайдите на страницу XML, для которой при помощи декларации
<?xml-stylesheet type="text/xsl" href="trans.xslt"?>
прописана таблица преобразования, совместимая с XSLT 1.0 - и наслаждайтесь результатом! (Тут, правда, следует учесть, что локальный файл не все браузеры корректно откроют - лучше разместить его на каком-то из ваших сайтов, либо поднять сервер локально и загрузить с него - применяет XSLT-стили к локальным файлам разве что FireFox последних версий...)

Так вот, в ситуации, когда XSLT 1.0 процессоров - хоть попой ешь, а XSLT 2.0 - в высшей степени скудно, Вашему покорному слуге недавно подумалось - а почему бы не использовать здесь примерно тот же паттерн, что я уже довольно давно и активно применяю по отношению к стеку технологий JavaScript/CSS/HTML, решая проблему кроссбраузерности без потери легковесности решений?

Тем более, что технология к этому располагает. XSLT-документы, фактически, представляют собой XML-документы, а потому к ним, в свою очередь, так же применим любой инструментарий работы с XML. А значит, можно написать XSLT-документ, который будет применяться к другому XSLT-документу, что бы преобразовать его. Зачем же может понадобиться преобразование одного XSLT-документа в другой? Для того, что бы в первом без особых заморочек использовать элементы XSLT 2.0 и XPath 2.0 запросы, а в результате преобразования получать пусть и более громоздкий, но корректный и работающий XSLT 1.0 документ.

Давайте прикинем, что нам для этого нужно?

Во-первых, нужно прописать в исходном XSLT 2.0 документе декларацию, ссылающуюся на документ преобразований уже для него. Т.е. начало нашей XSLT 2.0 таблички будет примерно таким:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="xslt2to1.xslt"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
В принципе, это, конечно, необязательно, но лучше это сделать, что бы не забыть, при помощи какого именно файла это преобразование будет осуществляться. Опять же - Altova XMLSpy последней, 13-й версии это дело игнорирует, не разрешая по кнопке "F10" трансформировать документ, который считает именно XSLT-документом, в отличие от того, как она позволяет поступать по отношению к обычным XML-документам, так что приходится осуществлять это преобразование, перейдя на вкладку преобразующего документа и там указывать исходный документ.

Во-вторых, нужно, собственно, создать преобразующий документ. Понятное дело, что этот документ крайне желательно так же иметь в виде XSLT 1.0, иначе полностью уйти от XSLT 2.0 движков не получится. Для начала давайте сделаем таблицу, которая будет полностью копировать исходный документ, а потом будем постепенно вносить в неё template`ы, что бы получить на выходе то, что надо:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>
Если применить эту таблицу к любому XML-документу - получим на выходе его полную копию! Хорошо, но нам же не нужна полная копия, так? Нам нужно преобразование. Для начала, нам нужно убрать, собственно, инструкцию процессору
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
, говорящую о том, что этот документ перед тем, как использоваться, должен быть преобразован согласно указанной XSLT-таблице стилей - выходной же документ, являющийся результатом преобразования, в свою очередь уже не нуждается в преобразовании и по-этому эту инструкцию нужно убрать.

Что бы сделать это, достаточно воспользоваться приёмом создания template`а, который имеет более специфическое XPath-выражение в атрибуте matсh, чем "node()" (поскольку селектор node() является максимально общим, практически любой XPath-запрос к тому же элементу будет более специфичным, так что сделать это не сложно ;) ). Тогда, согласно правилам XSLT, процессор для данного узла, либо атрибута выберет именно новый template, а не старый, именно из-за большей специфичности его XPath-запроса. Каким же должен быть этот template? Пустым, поскольку именно пустой template позволит убрать из конечного документа найденный узел. Т.е. убирается ненужная конструкция добавлением в таблицу преобразования следующего элемента:

<xsl:template match="processing-instruction()[local-name()='xml-stylesheet']"/>
Хорошо. Следующий шаг - изменение версии XSLT в документе, ведь мы преобразуем XSLT 1.0 в 2.0 . Это может быть сделано примерно так:
<xsl:template match="/xsl:*[local-name()='stylesheet' or local-name()='transform']/@version[.='2.0']">
    <xsl:attribute name="version">1.0</xsl:attribute>
</xsl:template>
Ну а дальше - непаханное поле, вполне достойное отдельного проекта на GitHub`е, который я собираюсь создать и потихоньку разрабатывать (когда найду на это время, конечно). В качестве примера можно рассмотреть реализацию довольно удобного атрибута "separator" в элементе value-of, который появляется в XSLT 2.0 и отсутствует в XSLT 1.0 . Этот атрибут задаёт символы, которые необходимо вставить между результатами выборки, проставленными в XPath-выражении атрибута "select". Например, выражение
<xsl:value-of select="'1'|'2'|'3'" separator=", "/>
выведет строчку:

"1, 2, 3"

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

<xsl:for-each select="'1'|'2'|'3'">
    <xsl:value-of select="."/>
    <xsl:if test="not(position()=last())">
        <xsl:text>, </xsl:text>
    </xsl:if>
</xsl:for-each>
Соответственно, преобразование может выглядеть примерно так:
<xsl:template match="xsl:value-of[@separator]">
    <xsl:element name="xsl:for-each">
        <xsl:attribute name="select">
            <xsl:value-of select="@select"/>
        </xsl:attribute>
        <xsl:element name="xsl:value-of">
            <xsl:attribute name="select">.</xsl:attribute>
        </xsl:element>
        <xsl:element name="xsl:if">
            <xsl:attribute name="test">not(position()=last())</xsl:attribute>
            <xsl:element name="xsl:text">
                <xsl:value-of select="@separator"/>
            </xsl:element>
        </xsl:element>
    </xsl:element>
</xsl:template>
Вот примерно как-то так...

P.S. Остался наиболее хитрый вопрос - о том, что делать с спецификой именно XPath 2.0 - запросов. Дело в том, что напрашивающиеся тут RegExp`ы в XSLT 1.0 (читай - XPath 1.0) не поддерживаются, что создаёт, мягко говоря, некоторые трудности. Что тут можно сделать? Либо отказаться от идеи использования в качестве процессора преобразования XSLT 1.0 процессор и по крайней мере для преобразования таблиц юзать процессор XSLT 2.0, либо использовать механизм расширений. Например, для Xalan`а пример работы можно подсмотреть тут.

пятница, 30 марта 2012 г.

Ещё одна модель наследования на примере реализации Set

В JavaScript принято великое множество разнообразных моделей наследования - практически каждая библиотека (MooTools, DOJO, JQuery, ExtJS и т.д.) предлагает свою, преследующую специфические цели и многие из них при этом игнорируют стандартную операцию instanceOf. Понимая, что вступаю на сверхконкурентное поле, всё-таки рискну вставить свои 5 копеек, потому что именно того, до чего дошёл в последнем проекте сам, я ни у кого не видел (хотя не исключаю, что просто плохо искал - так что те, кто считают, что я изобрёл велосипед, прошу меня простить великодушно за потраченное на чтение статьи время).

Одну из этих форм, не игнорирующую instanceOf, можно назвать классической для наследования в JavaScript, принятой в этом языке с момента его создания. Уже упоминавшийся мной Стоян Стефанов в своей книге "JavaScript Шаблоны" ("JavaScript Patterns") называет её "шаблоном по умолчанию" в разделе "Шаблонов повторного использования программного кода". Я долгое время использовал её в своих проектах. Не могу сказать, что она мне совсем уж во всём нравилась (об этом - ниже), по-этому я обдумывал, как её можно было бы улучшить, минимально изменив, но сделать это так, что бы она не потеряла ни своей краткости, ни своей понятности, ни красоты и что-то у меня в итоге, как мне кажется, получилось. :)

Классическую схему прототипного наследования можно продемонстрировать на следующем примере:

/** @constructor */
function A(x){
    this.x = x;
}
A.prototype.getX = function(){ return this.x };

/** @constructor
 * @extends A
 * @param y
 * @param [x=5] */
function B(y, x){
    this.y = y;
    if (typeof x !== 'undefined' && x !== this.x)
        this.x = x;
}
B.prototype = new A(5);
B.prototype.getY = function(){ return this.y; };

//Проверим:
var b1 = new B(1),
    b2 = new B(2, 1);
alert(b1.getX()); //5
alert(b1.getY()); //1
alert(b2.getX()); //1
alert(b2.getY()); //2
Здесь можно обратить внимание на то, что мы вынуждены вызывать конструктор предка лишь один раз и все его поля, установленные при этом вызове, автоматически становятся для нас как бы значениями по-умолчанию - мы проваливаемся по ссылке __proto__, считывая их, если происходит обращение к полученному объекту, а у него этого свойства нет. Т.е. в случае, если нам для них нужно иное значение, мы должны их заново устанавливать в конструкторе-потомке - помощи нам от конструктора-предка в этом нет никакой. Из этого вытекает, что в случае большого количества полей и при глубоком наследовании мы будем вынуждены каждый раз описывать все поля. И даже более того, что бы полноценно воспользоваться этим бенефитом в виде экономии на полях по-умолчанию, мы вынуждены прибегать к громоздкой конструкции проверки на наличие у нас соответствующего аргумента и проверки на его неравенство значению по-умолчанию:
typeof x !== 'undefined' && x !== this.x
По сути это означает, что наследуются только методы прототипа конструктора и значения полей по-умолчанию.

На проблему того, что нам приходится устанавливать все поля при том, что это можно было бы сделать при помощи конструктора-предка так же обратил внимание Стоян Стефанов, и для её разрешения предложил шаблон "Заимствование и установка прототипа", который приводит в той же 6-й главе той же книги. В соответствии с ним мой пример можно было бы модифицировать так:

/** @constructor
 * @extends A
 * @param x
 * @param y */
function B(x, y){
    A.call(this, x);
    this.y = y;
}
B.prototype = new A(5);
Таким образом, мы доверяем конструктору-предку сконфигурировать за нас объект, притворяясь, как будто он вызван в качестве конструктора, и даже если он в процессе этого задействует какие-то методы своего прототипа - они будут на месте, потому что прототип мы унаследовали. Хорошо, Стоян помог нам решить одну из проблем - мы добились повторного использования кода родительского конструктора и теперь не обязаны прописывать создание и инициализацию полей во всех потомках. Но осталась вторая проблема - в результате мы получаем объект, имеющий 3 набора свойств:
  • набор свойств базового объекта, применённых к нашему основному объекту (в нашем случае - поле this.x),
  • набор свойств, присвоенных нашему объекту его основным конструктором (в нашем случае - поле this.y)
  • набор свойств, аналогичный первому, но присвоенный прототипу (в нашем примере - это поле this.__proto__.x или, что более правильно с точки зрения ECMAScript 5, Object.getPrototypeOf(this).x)
В нужности первых двух наборов убедиться не сложно, но вот зачем нам третий?

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

typeof x !== 'undefined' && x !== this.x
А в варианте "заимствования и установки прототипа" - следующим, возможно ещё более неудобным образом:
/** @constructor
 * @extends A
 * @param y
 * @param [x=5] */
function B(y, x){
    A.call(this, typeof x === 'undefined' ? 5: x);
    if (this.x === Object.getPrototypeOf(this).x)
        delete this.x;
    this.y = y;
}
B.prototype = new A(5);
И, спрашивается, ради чего такие сложности?

Нет, я не спорю, что иногда значения по умолчанию могут нам для чего-то пригодиться, но, ведь, они нужны нам далеко не всегда. А классическая модель и Стоян Стефанов предлагают нам использовать их по сути постоянно и в случае, если они нам не нужны - либо старательно выискивать и удалять их вручную, как было показано выше, либо не замечать их и просто забывать об их наличии, по факту захламляя ими объектную модель, как обычно все и делают в реальных проектах.

А теперь задумайтесь - много ли в Вашей практике программирования на JavaScript было случаев, когда Вам были нужны значения по-умолчанию? Я сам для себя могу таких случаев припомнить немного. Но и в тех достаточно редких случаях, когда нам действительно удобно воспользоваться этим бенефитом в виде поля по-умолчанию, наш объект этого поля не будет содержать, а за его операцией this.x, фактически, будет стоять следующее действие интерпретатора JavaScript: он будет сначала искать это поле в объекте this и только потом - в объекте this.__proto__, и лишь там его, наконец, найдёт. Давайте теперь представим многоступенчатое наследование внушительного размера конструкторов с кучей полей в каждом, реализованное по такой вот модели - и совершенно точно получим нечто трудновообразимое...

Для того, что бы как-то оптимизировать такого рода структуру я несколько раз встречал у коллег костыльный код, подобный следующему:

for (var i in B.prototype)
    if (!this.hasOwnProperty(i) && typeof B.prototype[i] !== 'function')
        this[i] = B.prototype[i];
Не трудно увидеть, что смысл этой конструкции в том, что бы притянуть все установленные по умолчанию поля из всех прототипов объекта в сам этот объект, что бы их поиск по цепочке прототипов происходил быстрее - по мнению многих разработчиков, такой тюнинг вполне оправдан (иногда даже функции из прототипа подтягиваются так же). В итоге мы видим, как игнорируется и то призрачное преимущество полей по умолчанию, которое мы имели - даже тогда, когда они востребованы.

Кроме того, нельзя забывать о проблеме со ссылкой constructor, которая в случае этой модели на нижнем уровне просто теряется - вызов b1.constructor приведёт нас к функции A, а не к B.

По всему выходит, что все эти значения по-умолчанию в общем случае нужны нам обычно чуть меньше, чем собаке - пятая нога. В частном же случае, когда они действительно оказываются нужны, их, мне кажется, проще захарткодить. Но, используя такую модель, мы вынуждены везде втыкать эти значения, фактически отказываясь от их и так довольно призрачной использования при тюнинге. Так, может быть, "не стоит овчинка выделки"? :) Может, лучше будет, если мы будем сразу писать код, который будет оттюнен сразу?

Я предлагаю внести в эту схему изменение, направленные на её большую экономность и практичность: сэкономить на значении полей по-умолчанию.

Давайте подумаем, что нам для этого нужно? На мой взгляд, здесь важно понять, что в классической схеме ("шаблон по умолчанию" Стефана) мы вызывали конструктор предка с целью установки ссылки __proto__ и лишь побочным эффектом этого стала для нас установка в нагрузку ещё и значений полей в прототипе. Теперь давайте пойдём немного дальше и зададимся следующим, вытекающим отсюда вопросом - а существует ли иной, кроме создания экземпляра объекта, способ создать объект с ссылкой __proto__, ведущей на нужный нам прототип? Оказывается - да, существует! В ECMAScript5 у нас есть прекрасный метод Object.create, второй параметр которого необязательно указывать, а первый как раз-таки и содержит прототип вновь создаваемого объекта. Ссылку же 'çonstructor' можно прописать и вручную тут же, не отходя от кассы, воспользовавшись тем, что любое равенство в JavbaScript возвращает то, что в нём присваивается. Т.е. получаем такой компактный код:

function B(){/*...*/}
(B.prototype = Object.create(A.prototype)).constructor = B;
Всё - разобрались с проблемой: и объект прототипа у нас чистенький и ссылается он ссылками __proto__ и constructor куда надо - на прототип предка и на собственный конструктор соответственно, а если нам понадобится получить доступ к конструктору предка - это можно будет осуществить вызовом
Object.getPrototypeOf(Object.getPrototypeOf(b1)).constructor
, что, хоть и более громоздко, чем привычное и лаконичное b1.__proto__.__proto__.constructor, тем не менее является более правильным, чем прямое использование нестандартной ссылки __proto__, относительно которой даже на сайте Mozilla Developer Network уже чёрным по синему написано: "Deprecated", то бишь со временем это, по-видимому, будет убрано... Да, я понимаю, у меня у самого в связи с этим до сих пор некоторая ломка, но "надо, Федя, надо..."(с) :)

Единственная проблема, которая здесь возникает - это проблема с поддержкой старых браузеров, из которых актуальными остаются на сегодняшний момент IE6-8, не поддерживающие ECMAScript5. Для них придётся писать заплатки. Вот они для использующихся методов Object.create и Object.getPrototypeOf:

Object.create = function(proto, descriptors){
    if (typeof proto !== 'object' && typeof proto !== 'function')
        throw TypeError(proto + ' is not an object or null');
    var /** @private @type Object */ result,
        /** @private @type Object */ objPrototype = Object.prototype;
    Object.prototype = proto;
    result = new Object();
    Object.prototype = objPrototype;
    if (typeof descriptors !== 'undefined')
        Object.defineProperties(result, descriptors);
    return result;
};
Object.defineProperties = function(object, descriptors){
    for (var /** @private @type string */ i in descriptors)
        Object.defineProperty(object, i, descriptors[i]);
    return object;
};
Object.defineProperty = function(o, name, desc){
    o[name] = ('value' in desc) ? desc.value: null;
    return o;
};
Object.getPrototypeOf = function(o){ return o.__proto__; };
Увы, одним patch`ем отвертеться не удастся - для того, что бы последний метод работал, нужно в каждый конструктор вставлять ссылку "__proto__" выражением, которое я уже недавно демонстрировал в статье "__proto__ во всех браузерах":
if (!this.hasOwnProperty('__proto__'))
    this.__proto__ = B.prototype;
Почему Стоян Стефанов не обратил своё внимание на эту проблему - я не знаю. Может быть, счёл её не достаточно серьёзной, может - просто не придумал достаточно элегантного на его взгляд решения...

Вот что примерно получается, если сложить всё вместе (кроме patch`а для IE6-8):

/** @constructor */
function A(x){
    this.x = x;
}
A.prototype.getX = function(){ return this.x };

/** @constructor
 * @extends A
 * @param x 
 * @param y */
function B(x, y){
    A.call(this, x)
    this.y = y;
}
((B.prototype = Object.create(A.prototype)
    ).constructor = B
        ).prototype.getY = function(){ return this.y; };

//Проверим:
var b = new B(1, 2);
alert(b.getX()); //1
alert(b.getY()); //2
Этот метод хотелось бы продемонстрировать на примере недавно обсуждавшейся в статье "Коллекции в JavaScript - допиливаем setAddEventListener" JavaScript-аналоге коллекций типа множество (Set). Этот пример демонстрирует наследование от конструктора Array с переопределением метода push из несколько сомнительного расчёта на то, что множество будет наполняться исключительно посредством этого метода, хотя на самом деле, конечно же, может наполняться и прямой записью элементов по какому-либо индексу, но с этим уж ничего не поделаешь. А вот с функцией push кое-что поделать мы всё-таки можем:
/** Множество. При добавлении элемента массив проверяется на его наличие и добавление происходит лишь в
 * случае отсутствия в нём этого элемента.
 * @constructor
 * @extends Array */
function ArraySet(baseType){
    Array.apply(this, arguments);
}
/** Добавить элементы к множеству.
 * @param {...Object} arguuments */
((ArraySet.prototype = Object.create(Array.prototype)
    ).constructor = ArraySet
       ).prototype.push = function(){
    var /** @type Function */ push = Object.getPrototypeOf(Object.getPrototypeOf(this)).push,
        /** @type number */ length = arguments.length;
    for (var i = -1; ++i < length;)
        if (this.indexOf(arguments[i]) === -1)
            push.call(this, arguments[i]);
    return this.length;
};
А вот и необходимая заплатка для Array.indexOf в IE6-8:
Array.prototype.indexOf = function(value) {
    var /** @type number */ length = this.length;
        for (var /** @type number */ i = 0; i < length; i++)
            if (this[i] === value)
                return i;
    return -1;
};

среда, 29 февраля 2012 г.

Встраивание поддержки стандартов в DOM-дерево

В предыдущих статьях я приводил код, позволяющий добиться частичной поддержки стандартных методов в IE6-8, используя минимум ресурсов. Но встраивание его в DOM-модель производилось пользователем вручную. В данной статье я намерен показать, как этого можно было бы избежать, сделав стандартизацию IE прозрачной.

В одном из предыдущих постов я описал функцию, которая добавляет метод addEventListener к любому DOM-объекту при помощи нестандартного IE`шного метода attachEvent (в последующих постах я привёл для неё ряд улучшений - 1, 2). И всё бы хорошо, но сама по себе эта функция, очевидно, проблемы не решает, ведь теперь её нужно вызывать для всех объектов DOM-дерева, с которыми работает сценарий.

Можно было бы, конечно, вставлять эти вызовы в основной код, корректно отрабатывающий во всех браузерах, кроме IE6-8, но это его замусорит. Я уже не раз писал, что в современных проектах код часто итак достаточно сложен, так что отвлекаться при его написании ещё и на нестандартные браузеры часто слишком накладно как с позиции трудоёмкости написания, так и с позиции трудоёмкости его последующего изменения при отладке и расширении.

Так что я решил пойти несколько другим путём.

Я написал основной код на стандартном JavaScript`е строго в рамках стандартов, отладил всё в стандартных браузерах - IE9, а так же в последних версиях Chrome, Safari, Opera и FireFox, и лишь потом взялся за IE6-8, создав для них отдельный файл сценария в проекте. Добавление этого сценария можно произвести очень удобно - IE умеет понимать комментарии специального вида, которые диктуют ему различное поведение в отличие от его различных версий. Я написал так:

<!--[if lt IE 9]><script type="text/javascript" src="/_scripts/_lib/patch.js"></script><![endif]-->
Очень удобно, что все остальные браузеры считают этот код простым комментарием и не вставляют его, а IE 8`й и более ранних версий этот код вставляет и выполняет. Этот "комментарий" я вставил до объявления всех остальных сценариев, добившись, таким образом, что бы у меня была возможность в этом сценарии именно нужным образом подготовить браузер к выполнению стандартного кода, следующего дальше.

Итак, теперь осталось решить, как ранее уже описанную функцию setAddEventListener вызывать для всех DOM-объектов, что бы в нужный момент у них можно было вызвать стандартный метод addEventListener.

Легко понять, что попытка решить эту задачу в общем слишком глобальна, ведь модель DOM по сути представляет собой в большей степени коллекцию интерфейсов, а не конструкторов, как многие ошибочно полагают. В действительности у нас нет, например, конструктора Node и конструктора HTMLElement в браузерной среде - у нас есть только фабрика document, имеющая методы, которые возвращают объекты, соответствующие этим интерфейсам. Так что бесполезно было бы пытаться добавлять функционал к прототипам этих интерфейсов - они ни на что не влияют, а зачастую и вообще не существуют в браузерном окружении. Если мы попытаемся добавить стандартный, но отсутствующий в IE6-8 метод к Node.prototype или HTMLElement.prototype, надеясь, что данный метод появится у всех HTMLElement`ов, мы неминуемо будем разочарованы их отсутствием, а то и вообще получим ошибку при обращении к несуществующему объекту. Так что добавлением стандартного функционала надо заниматься на уровне фабрики - т.е. объекта document.

Однако сказать это проще, чем сделать. Ведь способов получить DOM-объекты в JavaScript масса и учесть все возможные их комбинации не представляется возможным! Вот, допустим, мы переопределим стандартный и наиболее часто встречающийся метод document.getElementById примерно так:

document.getElementById = function(){
    var src = document.getElementById;
    return function() {
        var /** @type HTMLElement */ result = src.apply(this, arguments);
        if (result !== null)
            setAddEventListener(result);
        return result;
    }
}();
Вроде бы, всё замечательно, но это, ведь, не единственный способ получения элемента. Вот вернули мы этим нашим методом в стандартный сценарий element, а в этом стандартном сценарии производится вызов свойства parentNode или nextSibling, и на него уже производится попытка повесить обработчик события стандартным образом - с этим как? Ведь, это же свойства, а не функции, и в IE6-8 у нас нет методов определения их в качестве properties - ни ECMAScript5`овских Object.defineProperties c Object.defineProperty, ни даже FireFox`овских нестандартных (и кстати уже deprecated) __defineGetter__ с __defineSetter__`ом. Так что мы не можем обработать их вызовы. Можем, конечно, написать так:
setAddEventListener(result)(result.parentNode)(result.nextSibling);
, но если в стандартном коде свойство parentNode будет вызвано несколько раз - и этот объект прописывать, а остальные?.. В общем, реальных комбинаций может быть очень много - это, конечно, конечное множество, но полноценный качественный алгоритм, который переберёт все возможные варианты, неминуемо серьёзно затормозит работу браузера, так что это решение не приемлемо.

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

document.getElementById = function(){
    var src = document.getElementById;
    return function() {
        var /** @type HTMLElement */ result = src.apply(this, arguments);
        if (result !== null)
            setAddEventListener(result)(result.parentNode);
        return result;
    }
}();
Кроме того, мне был нужен слушатель для элемента, возвращаемого конструктором Image, который по аналогии был реализован так же сходным образом, лишь с незначительными изменениями:
Image = function (){
    var src = Image;
    return function(){
        var /** @type HTMLImageElement */ result = new src;
        setAddEventListener(result);
        return result;
    }
}();