В продовження вивчення 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 перестане здаватися Вам чимось неосяжним і незрозумілим.
Я не претендую на звання «містер все знаю» або «вчитель року», так що в кінці кожного розділу я буду наводити посилання по темі, щоб Ви могли ознайомитися з описом приводиться теми з інших, можливо більш зрозумілих/авторитетних джерел.

Простору імен

Простір імен є однією з ключових концепцій у багатьох мовах програмування. Крім очевидної вигоди у вигляді запобігання колізій імен, розбиття нашого коду на простору імен дозволяє писати більш читабельний і акуратний код. Ми, як розробники на 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().

Посилання по темі:

  • Documentation: dojo-provide
  • Documentation: dojo-setobject
  • Книга Dojo: докладне керівництво. Стор 81

Модульність в 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 викине виняток. Власне, цим все сказано. Насолоджуйтеся.
Отже, ми отримали відмінний механізм управління нашим кодом: модульність, простору імен, завантаження скриптів на вимогу. Це вже повинно змусити мислити трохи ширше рівня «купа функцій в глобальній області видимості, причому злитих в ОДИН ВЕЛИКИЙ ФАЙЛ.јѕ»
Посилання по темі:

  • What dojo.require Does
  • Understanding dojo.declare, dojo.require, and dojo.provide
  • Documentation: dojo.require
  • Documentation: dojo.registerModulePath
  • Modules
  • Creating Your Own Modules
  • Module Helpers

ООП в 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 потрібно використовувати цей інтерфейс з двох причин:

  • з ним вже працює віджет нашої мегафичи
  • з таким інтерфейсом можна працювати не тільки в віджеті мегафичи але і в інших віджетах.

І знову всі щасливі.