Часть 2: Сохранение состояния на стороне клиента
Сен 20
В части 1 этой публикации, я познакомил вас с Cairngorm – компактной программной архитектурой, которая упрощает решение многих сложных повторяющихся задач в процессе создания насыщенных Интернет приложений, которые называют RIA.
В этой части я опишу задачи, которые возникают в процессе разработки RIA, а в частности как хранить состояние на стороне клиента. Я поясню это на примере создания интренет-магазина, который мы будем называть "Магазин Cairngorm". Вы изучите два фундаментальных паттерна архитектуры Cairngorm: паттерн Value Object и паттерн Model Locator. Под конец этой статьи вам будет понятно, насколько прозрачным можно сделать Flex-приложение, используя эти два паттерна.
ВВЕДЕНИЕ В "МАГАЗИН CAIRNGORM"
Flex Интернет-магазин демонстрирует возможности Flex-фреймворка. Здесь хорошо видно как можно использовать различные контейнеры для разметки, навигационные контейнеры, элементы управления, эффекты, связывание данных, менеджер Drag-and-Drop, валидатор форм, и менеджер истории. Кроме того, Flex магазин следует компонентной модели построения приложения во Flex, показывая каким образом можно создать ряд независимых компонентов и связать их воедино в событийной архитектуре. Flex магазин – замечательный способ познакомиться с декларативной разметкой используя MXML и бизнес логикой реализованной на ActionScript.

Рис. 1. Магазин Cairngorm
Я выбрал хорошо всем знакомую идею Интернет-магазина и реализовал ее как RIA уровня предприятия – приложение которое производительно, масштабируемо, и технически сопровождаемо – с помощью Cairngorm.
Магазин Cairngorm достаточно сложное Интернет приложение, которое выиграет от реализации на микроархитектуре Cairngorm. В части 3 я покажу этот момент истины – как легко вы можете добавить, в это Cairngorm приложение, функциональности и насколько предсказуемо будет его поведение после этого, а также насколько меньше всяких рисков по сравнению с простым приложением.
МАГАЗИН CAIRNGORM: ЧЕТЫРЕ ПРОБЛЕМЫ
Вместо академических рассуждений на тему паттернов проектирования, которые мы заложили в Cairngorm, я объясню самые важные проблемы, возникающие во время разработки RIA, и как можно решить их с помощью Cairngorm. Описывая сам процесс разработки, я попутно буду объяснять паттерны, из которых состоит сам Cairngorm.
Хороший технический архитектор сначала видит разработку приложения как решение бизнес проблемы, потом как систему, которая реализует это решение, потом как техническую архитектуру в этой системе, и уже в конце, как набор классов той архитектуры.
Такой метод разработки, каким предоставляет его Cairngorm, обеспечивает большую ясность реализации, нежели просто нырять в код.
На самом высоком уровне абстракции, есть всего четыре проблемы, с которыми сталкивалась группа Adobe Consulting, толи, создавая, ипотечные калькуляторы, системы пенсионных платежей, Интернет-банкинга, электронных платежей состоящих из одной странницы, полновесных систем электронной коммерции, или же интерактивных карт. Эти проблемы это:
- сохранение состояния на стороне клиента
- разработка представления данных
- реализация характерных функций
- вызов серверных служб
Для Магазина Cairngorm предъявляются те же требования и возникают те же проблемы.
Клиентская часть приложения показывает товар покупателю, корзина покупателя хранит все приобретенные товары, и пользователь должен пройти несколько стадий во время процесса оплаты. На протяжении всего это процесса, ваше приложении должно сохранять состояние на стороне клиента.
Отдельно взятое окно такого приложения имеет много разных элементов. Оно содержит и графический материал, и список свойств товара, и детальное описание; в корзину покупателя можно добавить разные товары; и серию форм, которые должен заполнить пользователь для оформления заказа. Все это касается разработки представления данных – View.
Также есть перечень особенностей такого рода приложения. Клиент должен иметь возможность выполнить следующее:
- просмотреть все продукты и получить детальное описание выбранного
- добавить или удалить из корзины продукт
- купить продукт в своей корзине
- пройти процесс оформления заказа
Эти свойства должны быть реализованными вами.
И последнее это то, что все необходимые данные ваше приложение получает из базы данных. И более того, при оформлении заказа, может понадобиться сохранения заказа в базе данных или же отправить их, скажем, в SAP. Таким образом, ваше приложение должно быть интегрировано с серверной частью.
Как вы можете видеть, наш Магазин Cairngorm – несмотря на свою уникальность в сфере бизнеса, работой с продукцией, поведением и свойствами – выглядит точно таким же, как и любое другое RIA, если мы рассматриваем его через призму четырех вышеозначенных проблем разработки.
В этой главе я буду касаться только первой из четырех проблем: сохранение состояния на стороне клиента.
ХРАНЕНИЕ СОСТОЯНИЯ НА СТОРОНЕ КЛИЕНТА
Главное отличие RIA от традиционных вэб-приложений это то, что клиент хранит свое состоние.
Разработчикам вэб-приложений очень хорошо знакома идея запрос-ответ – серфинга без сохранения состояния. Поэтому им приходится изобретать различные приемы для поддержки состояния клиента. Либо вы постоянно передаете одни и те же данные в HTTP запросе в процессе смены страниц, или вы используете URL-кодирование, или Куки чтоб сохранить небольшое количество данных на стороне клиента, или вы используете абстракции вроде J2EE сессий – все это механизмы сохранения состояния на стороне сервера и делание их доступными для клиента.
Однако с RIA разработчики освобождаются от кандалов HTTP запрос-ответ модели. Больше не надо использовать какие либо механизмы для сохранения состояния клиента – теперь он изначально сам хранит свое состояние. Если вы когда-либо создавали десктоп приложения или RIA, скажем, с помощью Swing или AWT, то все вышесказанное покажется вам известным – так оно и есть.
РАЗНИЦА МЕЖДУ СОСТОЯНИЕМ И МОДЕЛЬЮ В MVC
Многие разработчики знакомы с концепцией MVC (Модель-Вид-Контроллер) и, пожалуй, удивляются, при чем здесь Состояние. Все очень просто: состояние это и есть модель. Но на сколько это очевидно, это также порождает ряд других проблем с описанием модели в RIA.
Самая первая из них это то, что очень сложно расчленить состояние вашего приложения на строки, числа, булевы, и на прочие примитивные объекты. Здесь я не хочу вдаваться в обсуждение лучших практик объектно-ориентированного программирования. Хотя бы потому, что для понимания данного материала вы уже должны их знать.
Тем не менее, я решительно утверждаю, что те данные, которые содержит клиент – есть Состояние или Модель, или любой другой термин, имеющий семантическое значение. Что значит "семантическое значение" в данном контексте? Ну, например, если вы продаете рыболовные приманки, то ваша объектная модель будет включать «муха», «мотыль», «блесна». Если вы продаете ипотечные кредиты, то ваша объектная модель будет включать «продукт», «процентная ставка», «рефинансирование», «условия». Если вы продаете географические карты, то ваша объектная модель будет включать «маршрут», «координаты», «достопримечательности», «заправки».
СИНХРОНИЗАЦИЯ СОСТОЯНИЙ КЛИЕНТА И СЕРВЕРА
Есть одна проблема, связанная с RIA, уникальная для клиента хранящего свое состояние: состояние клиента обычно отображает состояние на другом конце линии – сервера. Давайте немного детальней поговорим об этом.
При разработке серверного приложения, одним из ключевых моментов есть то, что состояние хранится между двумя уровнями программной модели: между бизнес-уровнем и уровнем интеграции.
Уровень интеграции обычно включает в себя базы данных, мэйнфреймы, очереди сообщений, системы ERP (система управления предприятием – прим. перев.), CRM системы (системы управления продажами – прим. перев.) и другие информационные системы предприятия, которые хранят данные и управляют процессами характерными для конкретной проблемной области.
Все эти системы, при разработке серверного ПО, собираются воедино и предоставляются для доступа пользователю через промежуточный уровень, который называют «бизнес-уровень». Есть целое множество решений, призванных решить проблемы хранения состояния между двумя этими уровнями. Прежде всего, это из-за того, что представление данных на двух этих уровнях, может кардинально отличаться. Для примера, понятия сущности, отношения, видов, запросов, которые характерны для баз данных, должны быть отображены из модели базы данных на объектные модели данных приложений бизнес-уровня. Для этих задач могут использоваться такие технологии как Enterprise JavaBeans, Hibernate (8), ADO.NET.
Независимо от выбранного решения, разработчик несет большую ответственность за обеспечение целостности данных при передаче их между этими уровнями. Как только вы начинаете хранить одни и те же данные в двух разных местах, появляется опасность изменения данных в одном месте, которые не отобразились в другом, или же одни данные перезаписываются другими. Также разработчик должен заботится о параллельных процессах, блокировках, транзакциях, откатах и других средствах, гарантирующих то, что модель данных целостная в разных участках архитектуры.
Поскольку вы RIA разработчик, вы абстрагированы от сложностей взаимоотношений бизнес уровня и уровня интеграции. Вас больше беспокоит то, как обеспечить целостность данных в бизнес и презентационном уровнях.
Самое простое для вас в этом случае, это согласовать модель данных на клиенте и на сервере, это уже сократит ваши усилия для синхронизации данных.
ВВЕДЕНИЕ В ПАТТЕРНЫ VALUE OBJECT И DATA TRANSFER OBJECT
Всякий раз, когда вам нужно представить значение какой-то сущности в приложении, вы создадите класс, представляющий эту сущность. Например, в Магазине Cairngorm есть сущность представляющая продукт. Каждый продукт имеет множество атрибутов: имя, описание, цена, картинка, иконка.
Этот объект-значение (value object) играет двойную роль в RIA приложении. Кроме того, что он хранит значение на клиенте, он также может быть полезной структурой с помощью, которой вы можете обмениваться данными между различными уровнями приложения. Например, возможна такая ситуация, когда SQL-запрос к базе данных возвращает некий результат, который конвертируется в массив подобных объектов-значений.
Передавая такие объекты-значения между различными уровнями приложения, вы тем самым изолируете каждый уровень от внутренней логики работы его соседних уровней. Таким образом, промежуточному программному слою не будет нужды волноваться, где именно был создан полученный объект-значение: из SQL-запроса, хранимой процедуры или вызова вэб-службы. Повторно используя такие объекты-значения, вы расчленяете архитектуру приложения, передавая данные между уровнями как объекты со значением проблемной области («Продукт», «Рыба», «Карта»), нежели их техническими значениями («Запись», «Результат», «Группа данных»). Все это привело к именованию этого паттерна как Value Object (объект-значение), вместо Data Transfer Object (DTO, объект передачи данных).
Мы придумали Cairngorm еще до того, как термин DTO широко распространился, поэтому я называю бизнес сущности Value Object или просто VO.
Для примера, рассмотрите VO из Магазина Cairngorm (он располагается в org/nevis/cairngorm/samples/store/vo):
class org.nevis.cairngorm.samples.store.vo.ProductVO implements ValueObject, Comparable
{
public static var registered:Boolean = Object.registerClass( "org.nevis.cairngorm.samples.store.vo.ProductVO", ProductVO );
public var id : Number;
public var name : String;
public var description : String;
public var price : Number;
public var image : String;
public var thumbnail : String;
}
Конечно же, это не космические технологии. ProductVO есть не что иное, как набор атрибутов, которые описывают класс Product как продукт. Всякий раз, когда вы будете сохранять состояние на клиенте о продуктах, храните их в экземплярах класса ProductVO. Если приложение должно знать какой сейчас «продукт выбран», сохраняйте эту информацию в экземпляре ProductVO. Если приложение должно хранить список продуктов в корзине покупателя, сохраняйте эту информацию на клиенте в массиве экземпляров ProductVO.
Более того, если вы получаете от сервера список продуктов, или передаете продукты назад на сервер, передавайте их как объекты-значения. Кто-то может сказать, что здесь мы используем DTO. Это есть одно и тоже – помните это.
СОХРАНЕНИЕ ЦЕЛОСТНОСТИ МОДЕЛИ ДАННЫХ МЕЖДУ FLEX И JAVA
Серверное ПО, обычно представляет собой хорошо разработанную объектную модель. При использовании J2EE, скорее всего на сервере уже имеется Java модуль (package) объекта-значения, представляющий собой именно тот объект, который нужен клиенту.
В этом случае, сервер проделывает для Flex-приложения большую работу. Если вы хотите передать ActionScript объект-значение для Java приложения, Flex может автоматически создать эквивалент Java-объекта. Аналогично, когда Java приложения возвращает объект-значение клиенту, Flex конвертирует его в эквивалент объекта ActionScript.
И даже более, если объект не просто один объект, а составной граф из объектов, Flex выполнит всю работу по преобразованию в нужный объект, и даже сохранит связи между объектами. Таким образом, когда у вас есть объект ShopingCartVO, содержащий список ProductVO, а каждый ProductVO содержит список объектов AddressVO (адрес доставки и адрес для счета-факутры), и возможно объект DiscountVO, Flex обеспечит целостность объектов при передаче между RIA приложением и серверным ПО.
Для того чтобы обеспечить отображение (мапинг) серверных и клиентских объектов, нужно дать некую подсказку для Flex о существовании связи между объектами. В Cairngorm для этого нужно указать в каждом экземпляре объекта-значения, следующее:
public static var registered:Boolean = Object.registerClass( "org.nevis.cairngorm.samples.store.vo.ProductVO", ProductVO );
Это гарантирует то, что класс ProductVO на клиенте отображается на Java класс ProductVO.java, который находится в пакете org.nevis.cairngorm.samples.store.vo.ProductVO на сервере.
Использование XDoclet2 для ActionScript
XDoclet2 – это генератор кода ActionScript, написанный на Java. Джо Берковец из Allurent создал это проект с открытым исходным кодом, который создает ActionScript классы для VO из Java-аналогов. Вы можете больше узнать об этом проекте, а также скачать исходный код из SourceForge по адресу www.allurent.com/joeb/xdoclet2.
Резюмируя, Value Object обеспечивает:
- Использование лучшего подхода к программированию, представляя данные на клиенте в понятных терминах для данной предметной области
- Поддержка целостной модели данных между клиентом и сервером
- Использование возможностей Flex для транслирования объектов-значений между Java и ActionScript
Но получение данных клиентом от сервера, это только часть проблемы. А что вы будете делать с клиентскими данными и где вы будете их хранить, чтобы решить проблему полностью?
СВЯЗЫВАНИЕ МОДЕЛИ И ВИДА ВОЕДИНО
Смысл данных на презентационном уровне в n-уровневом приложении – это их отображение. А смысл R в слове RIA – это отображение этих данных насыщенным и разнообразным способом.
Связывание данных во Flex’e, одно из скромно умалчиваемых его особенностей. Связывание данных позволяет взаимодействовать двум объектам как источник и назначение. Изменение в объекте-источнике, сразу отображается в объекте-приемнике. Если объектом-приемником является визуальный компонент, а объектом-источником данные клиентского приложения, вы получаете мощное средство отображения состояния клиентского приложения пользователю в масштабе реального времени. Таким образом, вид клиентского приложения всегда останется актуальным, модель (состояние клиента) будет оповещать вид (пользовательский интерфейс), если вы свяжете модель и вид вместе.
В Adobe Consulting мы наблюдали за командами разработчиков процессом, создающими решения для обновления пользовательского интерфейса, когда подлежащая модель изменялась. Более того, эти разработчики легко попадали в путаницу, если эти данные отображались в разных местах и разным способом. Скажем, у вас есть последовательность чисел, которая была обновлена новыми данными из сервера. В зависимости от места в приложении, их надо показать либо в таблице или графиком, или в каком-то резюмированном виде согласно бизнес логике. Но если в результате рефакторинга, вы вынесете эти данные в модель, и используете механизм связывания, вы устраните много сложностей для процесса взаимодействия в RIA.
Более того, мы заметили, что каждый разработчик хранит нужные ему данные для своей части программы, в своем участке кода. Соответственно, если эти данные нужны другому разработчику в другой части приложения, ему придется заботиться, чтоб эти данные теперь отразились еще и в другом месте.
Часто мы видели, что программисты обращается к данным через длинную цепочку ссылок на компоненту, например: mx.core.Application.application.storeView.textualList.productGrid. Такой подход является невероятно хрупким – добавление или удаление компонента (storeView, textualList) потребует от разработчика обновления ссылок на productGrid.
Еще одним вариантом может быть передача данных от одного MXML-компонента к другому. Если рассматривать MXML компоненты как деревья, а данные будут их листьями, то некоторые листья будут нужны на разных деревьях. Единственное решение здесь, это найти общую точку ответвления для этих листьев, чтобы данные можно было передавать в виде:
<myView:MyComponent data="{this.data}" />
Но, как я уже сказал, это очень хрупкое решение. Это ведет к постоянным ошибкам из-за неправильных передач данных по цепочке где-то в одном месте. Отладка таких ошибок очень трудоемка, так как нужно будет проследить правильность прохождений данных между компонентами. Это напрасный труд.
ВВЕДЕНИЕ В ПАТТЕРН MODEL LOCATOR
Видя, как разработчики постоянно попадают в эту ловушку, команда Adobe Consulting осознала, что паттерн Model Locator является правильным решением для Flex программистов. Паттерн Model Locator уникален, поскольку он не заимствован из каталога паттернов J2EE. Мы создали его специально для Flex приложений. Нашей целью было создать единственное место, где будет хранится состояние Flex приложения и где View-компоненты должны находить данные, которые им нужно отобразить.
Наш паттерн Model Locator предполагает использование связывание данных (data binding), так что визуальные компоненты напрямую привязаны к состоянию клиента, которое хранится в одном экземпляре класса ModelLocator. Таким образом, как только модель изменятся в классе ModelLocator, все view-компоненты, привязанные к модели, получают уведомления (через внутренний механизм Flex Data Binding) и перерисовывают новые данные клиента.
Cairngorm предоставляет маркерный интерфейс для паттерна Model Locator и в Магазине Cairngorm мы имеем единственный класс, который описывает это интерфейс: org.nevis.cairngorm.samples.store.model.ModelLocator.
Рассмотрим несколько моментов использования класса ModelLocator. В Магазине Cairngorm есть понятие «выбранный продукт». Это предмет, который выбрал пользователь. Для этого наше приложение хранит в модели указатель на экземпляр класса ProductVO в атрибуте selctedItem, при этом панель ProductDetails использует его для отображения информации об этом продукте, а также он используется при добавлении продукта в корзину, когда пользователь нажимает кнопку «Add to Cart».
Вот так это объявлено в классе ModelLocator:
public static var selectedItem : ProductVO;
В третьей части вы научитесь, как использовать связывание данных, чтобы выбранный продукт всегда отображался в детальном описании продукта.
Также в ModelLocator вы найдете список продуктов для покупки в Магазине Cairngorm, к которому можете получить доступ отовсюду в приложении, сохраняя его в модели как массив:
public static var products : Array; // содержит объекты ProductVO
Также обратите внимание на то, как приложение хранит константы в ModelLocator. В нашем приложении эти константы определяют состояние приложения (которых всего три), следующим образом:
public static var workflowState : Number; //------------------------------------------------------------ public static var VIEWING_PRODUCTS_IN_THUMBNAILS : Number = 1; public static var VIEWING_PRODUCTS_IN_GRID : Number = 2; public static var VIEWING_CHECKOUT : Number = 3;
И наконец у нас есть компонента ShoppingCart, заключающая в себе необходимый функционал по добавлению и удалению продуктов в/из списка товаров для покупки клиентом. Этот объект хранится в атрибуте shoppingCart в ModelLocator и инициализируется в методе initialize().
Делая все атрибуты статическими в паттерне Model Locator вы получаете простую реализацию синглтона. Таким образом, к примеру, вы можете быть уверенным в том, что есть только одна инстанция ShoppingCart для одного клиента.
Единственный экземпляр ModelLocator инициализируется при старте главного приложения. В файле Main.mxml (точка входа в Магазин Cairngorm) у нас есть обработчик события creationComplete для Application:
<mx:Script>
<![CDATA[
import org.nevis.cairngorm.samples.store.model.ModelLocator;
private function onCreationComplete() : Void
{
ModelLocator.initialise();
}
]]>
</mx:Script>
Этот участок гарантирует, что все состояние клиента (например, его корзина) будет проинициализировано при старте приложения.
Дальше мы остановимся на ModelLocator поподробней. Но чтобы возбудить ваш интерес, скажу, что создавая свои компоненты, которые полагаются на данные на клиенте, вы не изобретаете запутанные способы, как достать эти данные, а просто привязываете эти компоненты к состоянию клиента через синглтон ModelLocator, в любой точке вашего приложения.
В качестве примера у нас есть компонента SideArea.mxml, содержащая компоненты ProductDetails и ShoppingCart. Как вы увидите в третьей части, ProductDetails требует выбранного продукта, чтобы отобразить его имя, описание, цену и картинку. Поскольку выбранный продукт сохраняется в ModelLocator в атрибуте selectedItem как экземпляр ProductVO, вы можете передать его в компоненту ProductDetails следующим образом:
<details:ProductDetails
id="productDetailsComp"
width="100%" height="325"
currencyFormatter="{ ModelLocator.currencyFormatter }"
selectedItem="{ ModelLocator.selectedItem }"
addProduct="addProductToShoppingCart( event )" />
Выделенная линия показывает, что в классе ProductDeatils есть атрибут по имени selectedItem, которые является экземпляром ProductVO. Мы только что использовали связывание данных (механизм фигурных скобок), чтобы гарантировать обновление ProductDetails как только изменяется значение selectedItem в ModelLocator. Я расскажу об этом механизме больше в третьей части.
ЧТО ДАЛЬШЕ
В этой статье я определил проблемы, которые возникают в насыщенных интернет-приложениях, независимо от проблемной области приложения. Я буду использовать их для того чтобы подробно описать модель разработки с Caringorm.
В частности, я выдвинул первую проблему как: хранение состояния на клиенте. Я также отметил взаимозаменяемость понятий «состояние», «модель», «данные клиента». RIA приложения позволяют вам освободится от парадигмы запрос-ответ для HTML веб-приложений. Они позволяют вам хранить данные на клиенте также как при работе с приложением. Отсюда вам будет понятна определенная ответственность за это состояние клиента.
В веб-приложениях ответственность за целостность и согласованность данных возлагается на сервер – между слоем интеграции и бизнес уровнем. Однако вы понимаете, что поскольку объект модели все равно уже есть на клиенте, вы можете использовать возможности Flex для отображения данных между ActionScript и Java, и хранить соответствующие друг-другу объекты как на сервере так и на клиенте. И даже без этого, представляя состояние клиента типизированными объектами с семантическим смыслом – нежели бездумными массивами строк, чисел и булевых – сильно улучшает читаемость кода программы и облегчает его поддержку.
Поэтому я включил в Cairngorm паттерн Value Object (VO, объект-значение), также известный в мире программистов как паттерн DTO (Data Transfer Object).
Имея осмысленный способ представления объектной модели клиента, будет гарантией того, что программистам будет понятно где хранить состояние клиента, как разделить данные клиента между различными компонентами (которые сами по себе могут иметь различный способ отображения данных), и как обеспечить вид клиента самыми последними данными его состояния.
В следствие этого, я представил паттерн Model Locator, впервые появившийся в Cairngorm, как решение того что состояние сохраняется в единственном классе, который доступен в любой точке Flex-приложения. Синглтон Model Locator гарантирует отсутствие дубликатов состояния клиента, дает большим командам разработчиков логичное место для хранения состояния приложения как объетов-значений, одновременно с этим использую мощный механизм связывания данных во Flex, чтобы гарантировать уведомление Вида когда Контроллер меняет Модель.
В третьей части я сосредоточу вас на Виде. Перед тем как подробно изучать фреймворк Cairngorm, я расскажу более подробно о том, как использовать Flex Data Binding с состоянием хранимым в ModelLocator.