« Сложные интерфейсы на javascript вместе Yahoo UI. Часть 5 | Анализируем в java загружаемые классы » |
Сложные интерфейсы на javascript вместе Yahoo UI. Часть 6
YUI - известная javascript библиотека для построения "богатых" пользовательских интерфейсов веб-страниц. Однако ее изобразительные средства были бы мало полезны, если бы в состав YUI не входили специальные модули позволяющие загружать в html-страницу информацию с сервера. Так в прошлый я начал рассказ о том, как YUI поддерживает идеи ajax. Мы научились загружать и отправлять на сервер информацию в различных форматах (text, xml, json) и даже, смогли асинхронно отправить на сервер файл. Сегодняшняя статья "зашлифует" некоторые аспекты построения ajax-сайтов.AJAX считается одним из способов уменьшить нагрузку на сайт, так может быть вы уже получали "письма счастья" от вашего провайдера с просьбой или уменьшить нагрузки на сайт или переходить на отдельный тарифный план. Еще ajax позволяет уменьшить объем трафика, передаваемый по сети (вы ведь уже не верите в сказку о бесплатном unlimited трафике для хостинга за 100 у.е. в год). Кроме того, ajax позволяет уменьшить время ожидания отклика приложения (страницы) в ответ на какое-то действие пользователя. И, наконец, ajax это просто модно. В общем, куда не кинь, без ajax-а никуда. Вся беда в том, что ajax стиль разработки имеет несколько недостатков диктуемых своей природой. И если вы используете ajax для отображения результатов поиска по сайту Васи Пупкина это одно, а если на ajax построено приложения для заказчика из буржуинии и за деньги, то это совсем другое дело.
Ajax расшифровывается как asynchronous javascript and xml. В слове javascript никаких подвохов нет, также как и в xml (с куда большим успехом сейчас вместо xml применяется json). А вот первая буква - "A" (asynchronous) принесет нам массу неприятностей. Есть известное философское утверждение, что "недостатки это продолжения достоинств", равно как верно и обратное. И javascript и flash стиль разработки по своей природе асинхронны. Т.к. работа выполняется в internet и любая пара "запрос-ответ" передаются по сети, то есть какая-то доля вероятности того, что в ходе передачи что-то "поломается". Например, когда вы открываете любую страницу в любой браузере, то он определяет список размещенных на ней ресурсов (картинок, css-стилей) и запускает несколько процессов, которые одновременно загружают информацию. Альтернативный вариант, когда все ресурсы грузятся последовательно, друг за другом, совсем плох: так если в середине очереди загружаемых файлов попадется одна большая картинка или картинка, расположенная на медленном канале сети, то все остальные ресурсы, стоящие в очереди после нее, будут простаивать. Таким образом, если вы хотите выполнить сразу после загрузки страницы какой-то сценарий, то перед этим нужно задаться вопросом: "а все нужные для выполнения этой работы ресурсы уже загружены или еще нет?". Уж сколько раз мне приносили сайты с плавающей ошибкой, когда javascript-код предназначенный для модификации визуального оформления html-страницы то работал, то нет, сосчитать тяжело. А вся беда в том, что javascript пытался изменить стилевое оформление той части страницы, которой еще нет (не успела она еще загрузиться в этот раз, вот в прошлый раз успела, а сейчас не смогла). На этой проблеме я уже многократно акцентировал внимание в своих статьях посвященных javascript (событие onDomReady), так что оставим ее и посмотрим, что происходит, после того как сайт с ajax-ом загрузился и пользователь пытается что-то на нем делать.
В мире прикладного программирования есть такое понятие как "application-транзакция" (бизнес-транзакция). Что такое транзакция вообще в программировании и в частности для СУБД, наверное, слышали многие. Вкратце: любая работа, которую выполняет приложение (и веб-приложение не исключение), состоит из этапов. Этапы нужно выполнить в строгом порядке. Состояние системы до начала транзакции и после ее завершения должно быть согласованным с бизнес-правилам. Например, работает на нашей фирме сотрудник Вася, или не работает - третьего не дано. А вот в ходе выполнения транзакции приложение может быть в "разобранном" (несогласованном) состоянии. Вот половину документов мы оформили, а половину еще не успели, но к концу рабочего дня все документы будут оформлены как это и требуется. Возможно, что в ходе выполнения работы произошел сбой (вот нашего сотрудника отдела кадров скрутил жесточайший насморк). Дальше оформлять документы он не может и уходит домой лечиться на неделю или две. А как быть с незавершенной транзакцией, читай, приемом сотрудника на работу? Что бы к нам не имели претензий проверяющие органы, мы должны транзакцию "откатить" т.е. кладем документы "под стекло" и просим Васю погулять эту недельку. Как вывод, транзакция в мире СУБД гарантирует целостность данных в таблицах. Т.е. все действия, которые выполняются в рамках процедуры приема сотрудника на работу, оформления накладных, закупок товаров выполняются в рамках границ транзакции. И если в середине процедуры происходит сбой, то все предшествующие действия будут отменены, а база вернется в исходное, непротиворечивое состояние.
Теперь вернемся к понятию бизнес-транзакции и тому, чем она отличается от транзакции в БД. Редко, крайне редко серьезный бизнес в ходе процедуры, например, приема Васи на работу, выполняет изменения только в одной БД. Чаще всего транзакции являются распределенными и в ней участвуют несколько СУБД (наверняка территориально размещенных в разных местах). Более того, в транзакции могут участвовать другие приложения, устройства и конкретные люди (например, пойти поставить штамп на документ у директора). Так что, если наш директор забыл дома печать, то все предшествующие действия придется "откатывать". Для простоты положим, что бизнес-транзакция распространяется только на одну базу данных, и даже в этом случае нас ждут неприятности. Прежде всего, традиционные desktop-приложения отличаются от web-приложений тем, что в них отсутствует постоянное подключение к источнику данных. Грубо говоря, при запуске desktop приложение подключается к СУБД, получает номер соединения и "держит" его до своего закрытия. Таким образом, когда начинается процедура транзакции, СУБД как бы "делает" снимок актуального состояния БД. И в случае необходимости выполнить "откат транзакции" трудностей не возникает. Веб-приложения построены по другому принципу: каждый запрос клиента создает новое (подчеркиваю) новое подключение к источнику данных. Когда работа скрипта завершается, то соединение "отдается" назад СУБД и теряется для нас безвозвратно.
Как вывод: все действия по модификации БД применительно к веб-приложениям нужно выполнить за один раз. Для примера: клиент должен заполнить некоторую форму, распределенную по нескольким страницам. Так, на первой странице он вводит сведения о себе, на второй о своей семье, на третьей об домашних животных. Мы не можем вносить сведения о заявке в СУБД до тех пор, пока не будет предоставлена вся информация. Мы ждем, пока не будет завершен ввод данных на третьей странице, а затем вносим сведения в БД. На словах все просто. Теперь задумаемся над тем, где сохранить промежуточную информацию? Как же скажите вы: есть такое понятие как сессия и информация привязывается к ней. Все это хорошо, но есть две проблемы. Первая очевидная - проблема надежности. Серьезные (а мы говорим именно о них) приложения работают под большими нагрузками и сбой может случиться в любую секунду. Когда это происходит, то нельзя обойтись надписью на экране "Упс, мы поломались, зайдите через пару часиков". Должна сработать система "прозрачного" для клиента переключения на запасной сервер, который как ни в чем не бывало, продолжит обработку запроса (бизнес-транзакцию). В идеале, обработка запросов должна выполняться набором серверов (кластером), которые плавно делят между собой нагрузку. И в случае сбоя одного из них выполняется переключение на другой сервер, который уже готов к работе. В частности у него есть своя копия данных, которые были введены клиентом в сессию того сервера, который только что "поломался". Итак: первая проблема - это репликация (распространение) состояния клиента между несколькими серверами, обрабатывающими запросы. И хорошо если все "состояние" клиента кодируется парой строк текста, а если он в ходе бизнес-транзакции загружает на сервер много-мегабайтные файлы?
Вторая проблема связана с необходимостью консолидировать бизнес-правила в одном месте. К примеру, когда пользователь заполняет анкету для банка, то на первой странице он указывает свой возраст, затем начинает заполнять остальные страницы и на последнем шаге (когда наше приложение пытается сохранить информацию в БД) узнает, что все напрасно, кредит ему не дадут, потому что он пенсионер. Собственно, это суровая правда жизни, но в чем суть проблемы для нас? В идеале мы должны вводимую информацию контролировать на каждом шаге. Т.е. заполнил пользователь поле со своим возрастом, а мы ему так не навязчиво в углу странички вывели надпись "увы, Василий Васильевич, но денег мы вам не дадим и не надо дальше заполнять анкету". Как следствие, бизнес-логика будет распределена по нескольким местам: проверки будет выполнять наш серверный код по мере прохождения процедуры заполнения анкеты. И еще одна дублирующая проверка будет выполнена самой СУБД при внесении информации в базу данных. Отказаться от проверки данных средствами СУБД не возможно - это приводит к стольким проблемам, что даже не стоит начинать разговора. Так что когда бизнес-правила некоторой процедуры поменяются, то нам нужно синхронно и согласованно внести правки в проверяющий данные код, размещенный в нескольких местах.
Описанные выше проблемы кажутся совсем не проблемами, когда задумаемся над тем фактом, что с веб-приложением работает несколько пользователей одновременно. Вернее работают не самим приложением, а данными, которые оно представляет им и данные эти постоянно изменятся самими пользователями. К примеру, вы все знаете об mediawiki, замечательной платформе для создания сайтов, наполняемых информацией самими пользователями (например, сайт wikipedia). Рассмотрим сценарий, когда есть страница со статьей, например, об арбузах. Посетитель Вася прочитал статью и нашел в ней несколько ошибок, затем движимый чувством любви к ближнему, он начинает исправлять статью. В этот же момент, посетитель Петя, также движимый заботой о точности статьи, пытается ее отредактировать, исправить ошибки, другие ошибки не те, что исправляет Петя. Мы попадем в классическую ситуацию "гонок". Когда два пользователя выполняют одну и туже работу, и последний из них сохранив свои изменения, затрет правки, выполненные первым редактором. И затрет он их не потому, что информация Пети не правильная, а потому что просто не знал о ней, не знал, что кто-то внес правки в документ, пока он его редактировал. Из описанной выше ситуации есть три выхода: "выигрывает последний" этот вариант, когда теряются правки, выполненные первым редактором, совсем не хорош. Вариант два - "выигрывает первый" - характеризуется тем, что когда первый редактор "забирает" статью для редактирования, то напротив нее в БД ставится специальная отметка (заблокирована для правки Васей). Так что, если Петя попробует открыть страницу и начать ее редактировать, то он получит сообщение об ошибке и будет вынужден ждать, пока Вася не завершит свою часть работы. Этот вариант не то чтобы плох, но и не слишком хорош. К примеру, Вася, заблокировав страницу, может отвлечься другими делами и забыть вернуть страницу в состояние "можно править всем". Можно было бы ввести понятие timeout-ов, промежутка времени после которого блокировка документа автоматически теряется, но все же лучше попробовать третью стратегию: "последний думает за всех". В этом случае, когда Петя запрашивает документ на редактирование, то вместе с его содержимым Петя (незаметно для себя) получает номер или ревизию документа (можно дату его последней правки). Когда редактирование файла будет завершено и данные отправляются обратно на сервер, то он обнаружит, что номер ревизии, которую правил Петя, устарел (на сервере лежит файл с большим номером ревизии). А раз, кто-то успел внести правки до Пети, то нужно отклонить операцию редактирования и вернуть Пете два документа: то, что было отредактировано Васей и то, что было отредактировано самим Петей. Видя перед собой эти два файла (а лучше видеть еще третий файл с первоначальным состоянием документа), Петя должен собрать из этих двух правок одну общую и сохранить ее на сервер. В некоторых ситуациях, подобный ход не возможен, например, изменения вносятся в двоичный файл (картинку), как уж тут объединить правки? Также есть шанс, что Петя будет невнимателен и неаккуратно выполнит слияние документов. Хотя здесь больше вопрос о создании удобного интерфейса пользователя минимизирующего шанс не заметить что-либо. Написав эти строки, я заметил, что часть рассуждений повторяет мои же слова, когда я рассказывал о системах управления версиями документов (серия статей про svn), так что на этом я закруглюсь.
Внимательный читатель, возможно, хочет сказать, что описанные мною проблемы являются общими для всех веб-приложений (также и для настольных приложений), а не только построенных на ajax. Все это верно, но ajax добавляет к описанным выше проблемам синхронизации действий нескольких пользователей еще проблему с синхронизацией действий самого клиента. В старые добрые времена (еще однозадачного DOS-а) пользователь работал с программой как? Нажал на кнопку - сиди и жди, пока результат не будет посчитан машиной и отображен на экране. А теперь можешь нажать на следующую кнопку и снова сиди и жди. Сейчас же все не так: вот зашел наш пользователь на сайт и нажал на кнопку "показать последние новости", а пока данные грузятся ajax-ом, чтобы не скучать жмет на кнопку "показать новые фотки ". Что-то загрузится первое, что-то попозже - не важно: ajax (асинхронный, одним словом). С одной стороны - это удобно, с другой - добавляет массу головной боли разработчиками. В описанном выше случае с загрузкой фотографий и новостей - никаких сложностей нет: обе операции не зависят друг от друга и действие с БД, которое они выполняют, - чтение. Как только одновременно можно делать несколько операций, работающих с одними и теми же данными (общим ресурсом на сервере) или вносящих изменения в БД - появляются сложности. Напоминаю, что ajax - значит, выполнение нескольких дел одновременно и в порядке не совпадающем с тем, в каком они были инициированы пользователем. Так хотя исходящий запрос на выполнение действия "A" ушел до отправки запроса "B", но на сервер они могут придти в ином порядке. Аналогично, ответ сервера на операцию "A" может с равной долей вероятности придти до или после ответа на операцию "B". В случае, фоток и новостей, как я говорил, перемешивание порядка действия большой роли не играло. А вот для серьезных приложений, изменяющих информацию, нам нужно заставить выполняться команды пользователей именно в том порядке, в котором они уходят на сервер. И если ответа на действие "A" еще не было получено, то отправлять на сервер действие "B" нельзя. Это так называемая синхронизация на стороне клиента. Есть еще вариант, когда упорядочение идет на стороне сервера (он сортирует приходящие запросы на выполнение работы в нужном порядке), но этот вариант сложен и применяется редко. Теперь задумаемся над тем как технически организовать подобное упорядочение. В прошлой статье я рассказал классе ConnectionManager из библиотеки YUI. Когда мы делаем с помощью его вызов, например:
r = YAHOO.util.Connect.asyncRequest("POST", "http://site.my", callback, "fio=vasya&age=12");
Connector.callMyMethodOnServer ("адрес скрипта", "параметры нужные для его работы")
Что же, сегодняшняя статья в серии получилась более теоретической, чем раньше. В следующий раз я завершу рассказ об ajax и перейду к рассмотрению других модулей YUI.
« Сложные интерфейсы на javascript вместе Yahoo UI. Часть 5 | Анализируем в java загружаемые классы » |