Антон Шевчук // Web-разработчик

Dojo для начинающих. Самая Важная Глава // JavaScript


В продолжение изучения Dojo Toolkit принимаю гостевой пост от Максима (aka Murzik)…

Данная статья действительна очень важна для понимания такого сложного и продвинутого инструмента как Dojo Toolkit, надеюсь моим читателям понравится стиль изложения Максима (мне уж точно понравился, так держать)…

Самая Важная Глава

Хотя данный материал целиком и полностью посвящается JavaScript и Dojo в частности, начать бы я хотел с истории развития языков программирования. Заметьте, в этой главе я часто употребляю слова “концепция” и “говнокод”. Это не просто так.

Немного о слове “концепция”

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

Немного о слове “говнокод”

Если применять понятие “говнокод” на уровне реализации функции/метода класса — то это откровенная неграмотность в использовании языка и/или в области решаемой задачи. Гениями конечно не рождаются, но если человек(программист) с течением времени продолжает “стоять на месте” …
Если это же понятие применять в масштабе всего приложения — то это попросту использование неправильных концепций или вообще полный отказ от их использования. Именно нехватка правильной концепции по мере роста кода и рождает саму концепцию.
Собственно, в этом ничего зазорного нет. Я считаю что это неизбежная ветвь развития. Главное чтоб само развитие имело место=) И мы находили правильные концепции.

О языках. Концепции

Каждый язык реализует определённый набор концепций. Я приведу пример самых популярных языков. А популярными они стали только потому, что использовали правильные концепции. Я сознательно опустил Basic, Fortran, SmallTalk, Lisp, PROLOG. Давайте начнем с самого начала.

А сначала был Assembler. Тут концепция предельно проста — уйти от байт-кода понятного машине и перейти на более понятный человеку набор команд. Что вам нужно знать чтобы писать код на ассемблере? Вам нужно полностью знать архитектуру железки под которую пишете, какой бит какого регистра установить чтоб ваш динамик за-би-би-кал. Не поверите, на ассемблере тоже можно писать “говнокод”. Оказывается недостаточно знать набор команд и архитектуру вашей каменюки, нужно организовывать код в виде процедур, иначе вы неизбежно наговнякаете код в котором через день сами ничего не поймете, не говоря уже о расширяемости (будь ты проклята, команда goto). Какая концепция возникла в результате? – Структурное и функциональное программирование, типизация данных.

Язык С. О Си! Ты принёс свет в этот мир!=) В языке Си удачно воплотились концепции структурного программирования, четкой типизации данных, абстрагирования от аппаратной части, механизм указателей. Как и на любом другом языке, писать “говнокод” — это просто. Просто продолжайте игнорировать структуры данных, указатели и ваш код съест всю память машины, и попутно, мозг другого разработчика. По мере развития, оказалось что структурного программирования тоже не достаточно. Проекты растут, код усложняется, управлять кодом всё труднее. Так появилась концепция ООП.

С++. Прямой наследник СИ. Всё так же поддерживает структурное и функциональное программирование, но появилась поддержка ООП. Также возникло понятие неймспейсов. Писать говнокод стало еще легче! По ООП проектированию и программированию написано немало книг. И все они рассказывают нам как не писать говнокод при использовании концепции ООП. Иногда код написанный на С++ с использованием процедурного и структурного подхода выглядит намного лучше чем “ООП” код (читай — “говнокод”). Не спорю, путь к пониманию ООП может быть долгим, и на этом пути мало кому удается избежать говнокода. Разве что бородатым дядькам, помнящим суровые времена господства Фортрана и перфо-карт. ООП -это скорее образ мышления, который надо выработать. Но пока нет понимания концепции, “правильных” привычек — мы обречены писать говнокод.

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

Python, Ruby. Эти языки являются “динамическими”. Эти языки объединяют такие концепции как: автоматическое управление памятью, “всё есть объект”, отсутствие четкой типизации данных, кроссплатформенность. Все эти концепции относятся к более высокоуровневой концепции – “быстрое прототипирование приложения”. Я сознательно опустил Java и PHP. Java имеет четкую типизацию данных и не имеет ничего общего с быстрым прототипированием приложения. PHP не является языком общего назначения и насколько мне известно, является единственным в своем роде “языком для Web”.

Из этого краткого экскурса в историю развития языков, можно понять, что программирование и языки программирования в своем развитии руководствуются девизом “Хотим педалить больше, быстрее, понятней!”. И для достижения цели такой мотивации придумываются новые концепции. Если Вы не знаете, не умеете применять, не можете придумать эти концепции или хуже того, не хотите их применять, то вы будете писать говнокод. В первом случае — пока не узнаете/не научитесь. Во втором — Вы обречены писать говнокод.

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

Библиотеки Dojo и jQuery. Концепции

Итак, после двух страниц не по делу, я таки добрался (почти добрался) до предмета обсуждения/описания. В нашем отделе широко применяются две библиотеки: Dojo и jQuery. Причем бытует мнение, что Dojo это страшный монстр, и не понятно что с ним делать. Цель данного документа — убрать этот барьер недопонимания насчет Dojo. Дабы у Вас не сложилось ошибочного мнения, говорю (вообще-то пишу… ну да ладно) сразу — Я не собираюсь ничего продавать. Это просто мнение и мысли. Ничего больше. Поскольку народу ближе к сердцу jQuery, рассмотрим концепции данной библиотеки.

jQuery. Концепции

Помимо очевидных вещей — простота и удобство работы с Dom и AJAX, в jQuery присутствует ряд фундаментальных концепций, которые лежат в основе библиотеки:

  • Чеининг(chaining)
  • Очень мощный движок выборки элементов по CSS селекторам, составляющий основу библиотеки
  • Простая и удобная система плагинов, как способ расширения. Как следствие, повлекшая за собой сопряженную концепцию -«нагугли плагин»
  • Чрезвычайно легкая и удобная работа с событиями, live events

Все эти концепции направлены на быстроту и удобство написания кода. При помощи jQuery можно легко и быстро выполнить операции с DOM практически любой сложности. Заметьте… это я Вам Dojo «продать» пытаюсь….

Но если немножко подумать….. jQuery практически не имеет средств для организации кода и управления кодом. Решить проблему организации и управления кодом в jQuery призвана система плагинов. И она с этим неплохо справляется……. пока Ваш плагин больше ни от кого не зависит или Вы не ловите себя на мысли типа «что-то многовато мой плагин на себя берёт…….». Как только появляются зависимости….. добро пожаловать в ад. Сбилди все зависимости в правильном порядке и ручками убедись что плагины ТОЧНО присутствуют. Вы конечно можете не распределять обязанности и свалить всё в один плагин….. А еще Вы можете написать свой механизм контроля кода. Так же, может возникнуть ситуация когда «два нагугленных плагина» должны работать с одним набором данных, но внутреннее представление данных у этих плагинов разное. К сожалению, разработчики jQuery и сопутствующих фреймворков, например, jQuery UI продолжают упорно игнорировать проблему организации кода.

А выводы такие: мало JavaScript кода? Основная преследуемая цель — манипуляция DOM? Вы можете легко решить все перечисленные мной проблемы при помощи jQuery? — тогда jQuery Ваш выбор.
А если Вас, как и меня, немножко озадачил список задач, и возникший список проблем связанный с этими задачами…. Ищите более подходящие концепции!

Dojo. Концепции

Очень многие из концепций принятых за основу в Dojo, направленны на организацию кода и управление кодом. Вот ключевые концепции:

  • реализация классов и классического механизма наследования (dojo.declare, dojo.extend)
  • «ленивая загрузка» скриптов (dojo.require())
  • поддержка пространств имен (dojo.provide())
  • физическое разделение Javascript кода на модули (dojo.require & dojo.provide)
  • модульность ядра самой библиотеки, в любой момент можно выкинуть или подключить модули
  • мощнейшая система билда скриптов с отслеживанием зависимостей через dojo.require (система билда представляет из себя консольную java¬утилиту)
  • наличие query движка для работы с DOM (dojo.query)
  • наличие мини движка для шаблонов(dojo.replace)
  • механизм абстакции данных (dojo.data)
  • локализация и интернационализация (дата, время, валюта, единицы измерения, переводы на язык пользователя). Чрезвычайно удобная, надо заметить

Dojo. Миф о отсутствии документации

Собственно, самого списка концепций и имён модулей реализующих данные концепции вполне достаточно чтобы «нагуглить» всё то, о чём я буду говорить дальше. Попробуйте погуглить по по ключевым словам. Вы удивитесь — документации просто море. Миф о том что по dojo очень мало документации зародился в те тёмные времена, когда версия dojo была 0.4. С тех пор прошло ~ 4 года. Сейчас, в большинстве случаев, этот миф является скорее средством самооправдания, чем объективной причиной.
Далее, я попытаюсь рассказать о концепциях принятых в dojo и реализации этих концепций. Надеюсь, что после прочтения данного подобия на «введение в dojo», dojo перестанет казаться Вам чем-то необъятным и непонятным.
Я не претендую на звание «мистер всё знаю» или «учитель года», так что в конце каждого раздела я буду приводить ссылки по теме, чтоб Вы могли ознакомиться с описанием приводимой темы из других, возможно более понятных/авторитетных источников.

Пространства имен

Пространство имен является одной из ключевых концепций во многих языках программирования. Помимо очевидной выгоды в виде предотвращения коллизий имен, разбиение нашего кода на пространства имен позволяет писать более читабельный и аккуратный код. Мы, как разработчики на PHP, были лишены данного блага до версии 5.3. Грубо говоря, мы этим еще не пользовались. Некое подобие пространств имен в PHP — PEAR-style class naming (например, Zend_Form_Element). Поскольку JavaScript лишь вспомогательный язык, а основным языком большинства из нас является PHP, всё что есть и чего нет в PHP напрямую влияет на то, как мы пишем JavaScript. Так что идея грамотного разбиения кода на пространства имен в JavaScript применяется далеко не всеми. И зря.

Концепция концепцией, но помимо концепции было бы неплохо иметь удобный инструментарий для организации пространств имен. Итак, встречайте: dojo.setObject() и dojo.provide().
Подробную документацию по этим двум функциям вы можете прочитать на сайте Dojo: http://dojotoolkit.org. Я опишу лишь как этим пользоваться для реализации концепции пространств имен в нашем коде.

В моем приложении, есть набор функций для форматирования различных величин: даты, денег, веса. Я захотел хранить их в следующем пространстве имен: app.util.formatter.* . Как это можно реализовать на Javascript? Примерно так:

var app = {util:{formatter:{}}} 
app.util.formatter.currency = function(){ 
// formatter code 
} 

А теперь скажите не задумываясь — сколько скобочек в первой строке кода? =) А вот как тот же результат достигается в Dojo:

dojo.setObject('app.util.formatter', {}); 
app.util.formatter.currency = function(){ 
// formatter code 
} 

dojo.provide() использовать еще проще:

dojo.provide('app.util.formatter');
app.util.formatter.currency = function(){ 
// formatter code 
} 

Помимо создания пространства имен, dojo.provide() выполняет ряд других полезных действий, о которых я расскажу в следующем разделе. Сейчас достаточно того, что она обеспечивает нам пространство имен. Естественно, внутри она использует dojo.setObject().

Ссылки по теме:

Модульность в Dojo. Ленивая подгрузка файлов модулей через dojo.require

Модуль — это пространство имен, логически объединяющее в один пакет набор классов/функций/переменных. Как правило, физически, такое пространство имен представляет из себя папку с файлами и другими папками. Dojo не является исключением из этого правила:

`-- root
    |-- dijit
    |-- dojo
    `-- dojox

Где dijit, dojo, dojox — пространства имен верхнего уровня.

Например выражение dijit.grid.DataGrid будет иметь следующее отображение на файловой системе: /dijit/grid/DataGrid.js.
В качестве корневого каталога, выступает каталог, на один уровень выше расположения dojo.js. Если Ваш dojo.js лежит в папке /www/js/dojo/dojo.js, то корневым каталогом, с которого dojo начнет поиск, будет /www/js.
Возможности dojo по организации модулей на этом не кончаются, там еще куча настроек. Можно задать корневой каталог вручную, можно задать нестандартное отображение модуля на файловой системе (my.big.module = /some/big/folder) и т.д. Моя цель донести концепцию. Подробное описание можно прочитать на сайте dojo в соответствующем разделе.
Код мы организовали….. что дальше? А дальше самое вкусное: dojo.require().
dojo.require() позволяет подгружать наш код, по требованию. Для javascript’а это вообще революция. Например, конструкция dojo.require(‘dojox.grid.DataGrid’) подгрузит файл /dojox/grid/DataGrid.js.

Механизм provide/require обеспечивает единоразовую подгрузку нашего скрипта и мы можем быть уверены на 100% что скрипт подгружен, иначе dojo выкинет исключение. Собственно, этим всё сказано. Наслаждайтесь.
Итак, мы получили отличный механизм управления нашим кодом: модульность, пространства имен, подгрузка скриптов по требованию. Это уже должно заставить мыслить чуть шире уровня «куча функций в глобальной области видимости, причем слитых в ОДИН БОЛЬШОЙ ФАЙЛ.js»
Ссылки по теме:

ООП в dojo

Dojo реализует классическую модель классов и наследования. Опять же, для javascript’a это очень круто.
По-моему, для большинства web-разработчиков (я не исключение), модель prototypal inheritance не очень внятная штука. И соответственно, дальше процедурного программирования мало кто продвигается. В лучшем случае, запихнут кучу функций в какой-то объект (а все объекты в один файл). За наследование вообще молчу.

Нет, мы должны знать prototypal inheritance модель наследования, но у многих из нас с этим трудности. Лично я не исключение.
Dojo.declare() делает за Вас всю «грязную работу» по созданию классов. Поскольку найти небольшой, всем понятный и не оторванный от жизни пример довольно трудно, я не буду приводить примеры с классами типа Shape или Animal. Почему-то со времен Страуструпа ничего умнее не придумали. Лично мне ничего не понятно на таких примерах. Давайте рассмотрим более реальную задачу и ее решение.
Всем из нас приходится решать проблему с AJAX коммуникациями. Очень часто, в ответ на посланный запрос, может прийти сообщение о статусе (выполнена ли запрашиваемая нами операция или нет), которое нам надо показать пользователю. Естественно, что помимо сообщения может прийти набор данных или html контент. Проблема в том, что прийти таки может и сообщение =) И это заставляет Вас писать в каждом обработчике ответа от сервера что-то типа (расценивайте приведенный код как псевдо-код):

onLoad: function(resp, status){ 
    if(isObject(resp) && isArray(resp.myData)){ 
        doSomethingUsefulWithData(resp); 
    } else if(isObject(resp) && resp.mesage){ 
        showMessage(resp) 
    } 
} 

На практике, условий может быть больше чем два. А прийти могут одновременно и данные и контент и сообщение. Это худший случай. Но когда он случится… Читабельность и понимабельность Вашего кода пострадает. Плюс ко всему, увеличится размер самой страницы, благодаря дополнительному JavaScript коду. Вобщем это всё конечно не смертельно, но когда Вам звонит заказчик и орёт в ухо: fix this fury bug! ASAP!!! или: I need this feature! ASAP!!! вы лезете в код….. и пытаетесь вспомнить «че тут происходит». И три(а то и больше) условных блока с каким-то кодом….. который кстати Вы и писали….. года пол назад(больше?)…. Или еще хуже — писали не Вы а Ваш сотрудник…. походу он уволился…. А тут еще заказчик…. С вазелином в одной руке и пряником в другой… Вобщем быстрому вспоминанию не способствует.

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

dojo.provide('core.Deferred');
dojo.declare('core.Deferred', null, {
    _callbackRegistry: null, 
    constructor: function(){ 
        this._callbackRegistry = [];
    },
    registerCallback: function(/*Function*/callback) { 
        this._callbackRegistry.push(callback); 
    },
    callback: function(/*Anything?*/data) {
        dojo.forEach(this._callbackRegistry, function(c){
            c(data); 
        });
    }
}); 

Мы объявили класс core.Deferred у которого null предков. Третий аргумент — это объект, который будет вмешан(mix-in) в наш класс. Думаю в особых пояснениях код не нуждается. Давайте теперь посмотрим как данный класс использовать и что нам это дало.
Итак, где-то где происходит инициализация всей клиентской части нашего приложения, а не только инициализация одной конкретной страницы (инициализация приложения отдельно, инициализация страницы отдельно), пишем следующее:

//app.js 
dojo.provide('app.global');
app.global.messageHandler = function(data) {
    if (isObject(data) && data.message) {
        app.global.notificator.showMessage(data);
    }
} 
.........................................................................................
dojo.require('core.Deferred');
app.global.deffered = new core.Deferred();
var d = app.global.deffered; 
     d.registerCallback(app.global.messageHandler);
     d.registerCallback(app.global.anotherStandartHandler); 
......................................................................................... 

Аналогичные действия могут быть выполнены и в файле инициализации страницы. Для добавления функций обратного вызова специфических для данной страницы. Или Вы можете создать для страницы отдельный объект core.Deferred. А теперь давайте пересмотрим наш AJAX обработчик ответа от сервера:

onLoad: function(resp, status){ 
    // do some stuff related to this request 
    // or you may add it to core.Deferred too 
    doSomeRequestRelatedStuff(); 
    // and then call our callbacks, may be in response message? 
    app.global.deferred.callback(resp);
} 

Если у Вас до десятка ситуаций (по-моему, это потолок), которые необходимо предусмотреть в любом обработчике ответа, мы заметно выиграли в читабельности и понимабельности кода практически не принося в жертву производительность. Да и писать меньше.
Сразу отвечаю на заявление что «сообщения — это единственная вещь где может понадобиться подобный подход» — не единственная. Сервер может попросить клиентскую часть сменить часть глобальной/локальной конфигурации. А это может повлечь за собой какие-либо действия. Например — поменять форматирование даты/чисел из-за изменения локали. На стороне клиента. Без перезагрузки страницы. Нет, я не сказки рассказываю. Dojo это может.
Задачу решили, решили при помощи классов. Но что это нам дало? А дало нам это следующее:

  • класс Deferred в пространстве имен core
  • этот класс лежит в отдельном файле Deferred.js, который находится в /js/core/Deferred.js а не в одном большом файле all_my_code.js Кто ковырял подобные файлы, тот поймёт.
  • мы можем легко расширить функционал при помощи того же dojo.declare
  • мы можем подгрузить его только тогда, когда он нам действительно понадобится при помощи dojo.require(‘core.Deferred’)
  • мы получили интерфейс

И всё это мы получили довольно просто:

dojo.declare(/*String*/'className',/*null | Obj[]*/parents,/*Obj*/props)

Если я Вас таки заинтересовал, давайте вернемся к объявлению класса, там есть пара не совсем очевидных моментов. Итак, код в студию:

dojo.provide('core.Deferred'); 
dojo.declare('core.Deferred', null, {
    _callbackRegistry: null,
    constructor: function() { 
        this._callbackRegistry = []; 
    },
    registerCallback: function(/*Function*/callback)  { 
        this._callbackRegistry.push(callback); 
    },
    callback: function(/*Anything?*/data) {
        dojo.forEach(this._callbackRegistry, function(c){
            c(data); 
        }); 
    } 
}); 

Взгляните на две первые строчки после dojo.declare. Объявление переменной («типа» protected) и объявление функции конструктора, в которой происходит инициализация переменной. В чем подвох? Подвох в том, что если вы сделаете _callbackRegistry:[] не в кострукторе класса, вы получите __статический__ член класса core.Deferred. Т.е. Если Вы инстанцируете два объекта core.Deferred и начнете пихать в него функции обратного вызова, то оба объекта будут разделять __один и тот же__ callbackRegistry. Это небезысвестный и плохо понятый prototype chaining в JavaScript. Если вы не проинициализируете член класса в самом объекте во время его создания, то его там и не будет. Вместо этого яваскрипт пойдёт искать его в прототипе предка. И так по цепочке.

Это касается только объектов и массивов. Числа и строки можно объявлять без инициализации в конструкторе. Странно, да? Цитата:

When using dojo.declare to build your own fancy widget there is one thing (besides others) you should keep in mind: Arrays and objects as member variables are stored as references and not copies.

Собственно с этим можно жить=). А если вам нужен __статический__ член класса, так вообще замечательно. В общем, Вы должны иметь ввиду данную особенность.
Я привел далеко не все возможности dojo.declare. Их больше. Вы сможете прочесть о них по ссылкам в конце раздела посвященного dojo.declare. А пока, давайте рассмотрим еще один небольшой и немного мутный пример.
Помните, я в качестве одного из преимуществ использования dojo.declare указал на тот факт что мы получили интерфейс. Давайте рассмотрим случаи, когда это может быть полезным.

Итак, у Вас есть проект. Пишете Вы его с Васей и Петей. Вас (а не Васю или Петю) попросили сделать фичу. Скажем, эта фича заключается в следующем:

  • дернуть данные с сервера
  • обработать вернувшиеся данные
  • вывести результаты запроса в качестве таблички
  • несколько моментов:
    • сервер может быть не Ваш, и вернуть данные как Вам удобно, попросту не получится.
    • сервер может быть Ваш, но сервис установленный на нём может быть не Ваш…. так что вернуть данные как Вам удобно не получится
    • сервер может быть Ваш, обработать данные как Вам удобно можно…. вот только сервер итак загибается. Так что обработать данные на стороне сервера что-то никак не получается…
  • и совсем фантастика: помимо всех указанных причин, на JavaScript обработать данные будет попросту удобней
  • в конце концов, прекратите недооценивать JavaScript.

Итак, вооружившись dojo.declare и собственным мозгом Вы начинаете думать.
Зная, что Ваш заказчик может сказать на белое что оно черное… И вообще хз…. мало ли че ему там в голову взбредет….
А надумали Вы следующее:

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

Всё по духу ООП. Непривычно, да? Привыкайте, Вам понравится.
Дабы не усложнять и без того не самый простой пример, обработчик данных и контроллер будут одним и тем же объектом. Итак, разрабатываем наш супер виджет:

dojo.provide('myapp.superfeature.Widget');
dojo.declare('myapp.superfeature.Widget', null, { 
//summary: 
// Это абстрактный класс моего мега виджета, 
// Виджет должен отображать отображать данные принятые 
// от сервера, и прошедшие обработку 
//decription: 
// Виджет дергает данные с серве при помощи dataSource. 
// Затем преобразовывает полученное при помощи 
// метода processData и отображает данные при помощи dataView 
//example: 
// | var widget = new myapp.superfeature.Widget(args); 
// | widget.doWork(); 
//dataSource: любой реализующий myapp.data.Interface
// потому что я точно знаю что у них всех 
// есть метод getData() 
// т.е. объект, реализующий интерфейс myapp.dataSources 
dataSource: null, 
//dataView: любой объект, у которого есть метод render 
// т.е. любой объект, реализующий интерфейс ViewRenderer 
dataView: null, 
//dataForRendering: Anything 
// Данные, получившиеся в результате 
// работы this.processData() 
dataForRendering: null, 
//_connections: Array 
// Массив, который хранит 
// все указатели на соединения 
// с другими объектами 
_connections: null, 
// myapp.superfeature.__constructorArgs = { 
// dataSource: myapp.data.Interface 
// Объект, который знает как дернуть сервер, 
// и как интерпретировать полученные данные 
// dataView: Object 
// Объект, который умеет отображать данные 
// и имеет метод render 
//} 
constructor: function(/*myapp.superfeature.__constructorArgs*/args){ 
    dojo.mixin(this, args); 
    this._connections = []; 
    this._connections.push(
        dojo.connect(this.dataSource, 'dataLoaded', this, 'processData') 
    );
},
processData: function(data){ 
    // summary: Данный метод считает SUM по полю population 
    // всех городов, принадлежащей одной стране 
    var processedData; 
    // итерируем по data.items 
    // и заполняем processedData 
    //......................... 
    //вежливо просим вьюху отрендерить получившееся
    this.dataView.render(processedData); 
},
doWork: function(){ 
    //summary: 
    // Получаем даные 
    // Обрабатываем 
    // Рендерим 
    // Вуаля! 
    this.dataSource.getData(); 
} 
}); 

Давайте теперь разберемся че это мы такое написали в приступе горячики… пардон… творческого порыва. Первое что сразу бросается в глаза, по сравнению с уже имеющимся у нас опытом по использованию dojo.declare — код конструктора. Давайте разберем его:

constructor: function(/*myapp.superfeature.__constructorArgs*/args){ 
    dojo.mixin(this, args); 
    this._connections = []; 
    this._connections.push(
         dojo.connect(this.dataSource, 'dataLoaded', this, 'processData') 
    ); 
} 

Первая строка в коде конструктора «вмешала» все переданные параметры в конструктор класса непосредственно в сам класс. Если мы передали в конструктор объект вида {dataSource: {}, dataView: {}}, то все эти атрибуты передаваемого объекта будут доступны как this.dataSource и this.dataView внутри класса. Ну и снаружи тоже=) Только замените this на имя объекта. Идем дальше….
Вторая строчка инициализирует массив _connections в который мы будем складывать указатели на соединения с другими объектами. Другими словами — это указатель на соединение с событием, которое мы выполняем при помощи dojo.connect в третьей строчке. Давайте остановимся на событиях. И обсудим данное событие в частности.
Если Вы пользовались jQuery, то знаете как легко и просто там работать с событиями:

$('#bar').bind('click', {msg: message}, function(event) { alert(event.data.msg); }); 

jQuery создает соединение для выбранного элемента, и кладет указатель на соединение во внутреннее хранилище, ассоциированное с данным элементом. А потом зовём $(‘#bar’).unbind(‘click’) и jQuery удаляет это соединение для этого элемента. В общем, вся магия происходит «за сценой». Это хорошо когда Вы соединяетесь с DOM элементом. Но когда Вам нужно забиндиться к кастомному JS объекту внутри своего класса… Может вы меня просвятите? Я не знаю как это сделать при помощи jQuery. Разве что ассоциировать DOM элемент с классом, который вообще в нем не нуждается.
Оставляем jQuery, возвращаемся к нашему коду.

Чуть-чуть вмешаюсь с jQuery – у меня вполне заработал следующий пример:

var obj = {
    test:function() {
        console.log('obj.test');
    }
}

$(document).ready(function(){
    $(obj).bind('someEvent', function(){
        console.log('obj.someEvent');
        this.test();
    });
    
    $('#test').click(function(){
        $(obj).trigger('someEvent');		        
    });
});

Но что происходит внутри, действительно не совсем понятно…

Итак, мы забиндили обработчик события «processData» который является методом нашего класса myapp.superfeature.Widget, и выстреливает при возникновении события dataLoaded в объекте dataSource. Зачем нам это нужно? Затем что наш dataSource будет делать асинхронный запрос на сервер, и нам нужно получить данные именно тогда, когда они придут от сервера. Итак, при помощи такого механизма, мы имеем доступ к асинхронно получаемым данным, которые нам требуются в нашем классе.
Дальше всё банально: получили, обработали, отрендерили.

Давайте теперь поговорим о dataSource.
Наш источник данных будет простой оберткой над стандартным AJAX транспортом, предоставляемым dojo. Зачем нам нужна эта обертка? Эта конкретная обертка будет работать с конкретным форматом данным, а именно c JSON. И отдавать полученные данные наша обертка будет тоже в унифицированном формате. Итак, мы в творческом порыве принялись писать наш источник данных. Но прежде чем его писать…. мы поняли что мы можем получать данные не только в JSON но и в XML. Это уже другая обертка, не правда ли? Но мы хотим по прежнему иметь метод getData, и не важно обертку для какого формата мы используем. И вот тут нас посещает гениальная мысль интерфейс:

// js/myapp/data/Interface.js 
dojo.provide('myapp.data.Interface'); dojo.declare('myapp.data.Interface', null, { 
    //summary: 
    // Единый интерфейс для всех классов myapp.data.*
    // Любой из myapp.data.* должен наследовать от этого класса
    // В такой способ, я точно, знаю на какой набор функционала 
    // могу расчитывать 
    // myapp.data.__Response = { 
    // items: [ 
    // /*Any Object*/ responceItem1, 
    // /*Any Object*/ responceItemN 
    // ] 
    //} 
    getData: function(){ 
        //summary: 
        // Метод для получения данных с сервера 
        // должен возвращать объект типа 
        // myapp.data.__Response 
        throw new Error('Интерфейс не реализован: myapp.data.interface.getData')
    },
    dataLoaded: function(/* myapp.data.__Response */ data ){ 
        // summary: 
        // Точка расширения, 
        // к которой можно присоединиться 
        // при помощи dojo.connect() 
        // Чтобы иметь возможность получать 
        // доступ к загруженным данным 
    }
}); 

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

// js/myapp/data/JsonSource.js 
dojo.provide('myapp.data.JsonSource');
dojo.require('myapp.data.Interface');
dojo.declare('myapp.data.JsonSource',[myapp.data.Interface], { 
    //getData: Function
    // смотри: myapp.data.Interface 
    getData: function(){ 
        // summary: 
        // Делает запрос на сервер, 
        // который должен данными 
        // в JSON формате 
        // и возвращает объект типа: 
        // myapp.data.__Response 
        dojo.xhr(this.method, this.args); },parseResponce: function(response){ 
            // summary: 
            // Парсит JSON ответ от сервера 
            // и преобразует его к виду 
            // myapp.data.__Response 
            var resp = eval(response); 
            // если полученный объект Array 
            // то приводим его к виду 
            // myapp.data.__Response
            if(dojo.isArray(resp)){ 
                // эквивалентно записи: 
                // this.resp = {}; 
                // this.resp.items = resp 
                dojo.setObject('resp.items',resp, this);
           } else if(dojo.isObject(resp) && dojo.isArray(resp.items)){ 
                // данные итак пришли 
                // в формате myapp.data.__Response
                this.resp = resp;
           } else { 
                // хз че это пришло
               throw new Error('myapp.data.JsonStore: Сервер ответил неверным форматом данных'); 
           } 
           // вызвать точку расширения, 
           // так что присоединенные к ней 
           // при помощи dojo.connect() функции 
           // отработают 
           this.dataLoaded(this.resp); 
     }
});

Ничего военного или сверхъестественного. Просто обернули AJAX транспорт в удобный для работы интерфейс и использовали dojo.connect для доставки данных всем страждущим. Код для dataView приводить не буду. Думаю у всех хватит опыта и фантазии чтоб представить как из массива данных можно сгенерить таблицу. В деталях это слишком сложно и (тут и сейчас) попросту не нужно. Итак, как результат (гипотетический): вы довольны потому что написали классный код, заказчик доволен потому что это работает (хотя на код ему ложить хотелось).

Но это ж не конец! Через некоторое время, Ваш заказчик Вам говорит: “Парни, мой проект начинает пользоваться спросом. Мои новые клиенты всем довольны. Вот только они хотят вместо таблички со статистикой — график. Да вот знаете в чем беда? Моих первых клиентов вполне устраивает табличка. Не могли бы вы что-нибудь придумать?”
И тут очень многое зависит от того, как мы среагируем в подобной ситуации. У заказчика появляется перспектива, а значит и у Вас тоже, и у Вашей фирмы.

Если Вы заранее потрудились написать код подобный тому, что я привел выше, Вам не составит особого труда дописать новый объект view, которая бы рисовала не табличку а график, не график а дерево. Наша архитектура предусматривает лёгкую смену объекта, отвечающего за отображение. Да, остается сложность реализации самого компонента view, но это __намного__меньше__ чем повторная реализация всего кода.

Да и еще одна незадача: Вы жутко загружены другой фичей, очень сложной и требующей от Вас всего внимания. Вы не можете отрываться на реализацию новой вьюхи. Но вы работаете с Васей и Петей. Вася тоже занят…. Петя не сильно шарит в javascript’е, вот сервисы он пишет отличные! Но к сожалению, реализацию вьюхи прийдется поручить ему.
И вот Вы говорите Пете: “Петь, там нужно вместо таблички отрендерить график. Займись. Там есть myapp.superfeature.Widget который юзает myapp.data.JsonSource и myapp.view.TableRenderer. Просто замени myapp.view.TableRenderer на ChartRenderer, в остальное можешь не вникать”

И вот, мужественный Петя лезет в Ваш код, стиснув зубы… и роняет челюсть на пол: ого! Да тут куча комментов. Всё понятно и удобно. Всего-то, написать новую вьюху. Че там за dataSource такой его вообще не гребет.

И вот, Петя практически не отвлекая Вас от работы, ибо по комментариям итак ясно что и как работает, педалит новую вьюху и опять все счастливы! Заказчик счастлив — его потребители удовлетворены. Петя счастлив — не пришлось писать кучи вещей в которых он плохо разбирается и вообще не пришлось писать кучи вещей…. Вы счастливы — Петя не дергал Вас каждые 5 минут и не спрашивал что это за х….
А еще через неделю, Вы отправили Петю писать обертку для AJAX транспорта, которая умела бы работать с XML… А Петя глянул на комменты… на предоставленный Вами интерфейс… И понял! Понял Ваш код, понял его архитектуру и еще одну штуку. Что в его обертке для XML нужно использовать этот интерфейс по двум причинам:

  • с ним уже работает виджет нашей мегафичи
  • с таким интерфейсом можно работать не только в виджете мегафичи но и в других виджетах.

И снова все счастливы.

© Антон Шевчук 2007-2016