1. Введение
Для настольных приложений шаблон проектирования Model-View-Controller
(MVC) стал стандартом, и некоторые из них было бы практически невозможно
сделать качественно иначе. В то же время для очень многих
Web-разработчиков понятие MVC может оказаться новым. Существует
несколько общепринятых типовых шаблонов и подходов к разработке (patterns
and practices). Их применение всегда несколько усложняет приложение при
этом давая ему такие качества как: удобство разработки, тестирования,
легкость последующей модификации, структурированность и просто красота
кода. Данная статья обращает внимание на некоторые возможности ASP.NET,
в какой-то степени новые для Web-технологий. Делается попытка сгладить
различия между Web- и настольными приложениями.
2. Немного об MVC
Учитывая то, что именно Web-разработчики часто не знакомы с шаблоном
MVC, будет полезно сделать для них некоторое отступление.
Назначение большинства компьютерных систем – получение данных из базы
и отображение их пользователю. Затем пользователь выполняет какие-либо
действия и система сохраняет данные либо модифицирует их. Так как
основной обмен данными происходит между хранилищем данных и
пользовательским интерфейсом, очень часто функциональность объединяют,
причем даже извлекают из этого пользу в виде повышения общей
производительности системы и уменьшения объема кода. Однако данный
подход хранит много подводных камней. Часто возникает необходимость
независимой модификации пользовательского интерфейса или бизнес-логики.
Дальнейшее усложнение приложения требует создания сложной объектной
модели и постоянное ее изменение.
Решение проблемы – создание некоей модульной структуры. Первое – это
использование простых шаблонов (templates). Второе – применение
объектной модели, если это позволяет используемая технология. Третье –
создание многослойных (multi-tier) приложений.
Требования
- Пользовательский интерфейс Web-приложений имеет тенденцию к
более частому изменению, чем бизнес-логика (хотя встречается и
обратное).
- Очень часто приложению приходится отображать одни и те же данные
несколькими способами.
- Разработка внешнего вида Web-приложения и его «механика» требуют
чтобы этими частями занимались по отдельности (скажем, Web-дизайнер
и команда Web-разработчиков).
- Пользовательский интерфейс обычно выполняет следующее:
- представляет данные (получает и форматирует их)
- изменяет данные (полученные от пользователя данные
передаются бизнес-логике для выполнения необходимых изменений)
- В Web-приложениях один запрос часто генерирует несколько
совершенно не связанных между собой команд (например, добавление
пользователя и последующий вывод списка пользователей).
- Пользовательский интерфейс по своей природе является более
«зависимым» от платформ и обозревателей интернет.
- Качественное тестирование бизнес-логики, тесно связанной с
пользовательским интерфейсом, затруднительно.
Решение проблемы
Шаблон MVC позволяет разделить бизнес-логику, представление и
обработку действий пользователя на три отдельных класса.
- Модель (Model). Модель отвечает за поведение приложения,
предоставляет данные (обычно для View) а также реагирует на запросы
изменить свое состояние (обычно от контроллера).
- Представление (View). Отвечает за отображение информации.
- Контроллер (Controller). Интерпретирует данные, введенные
пользователем, и информирует модель и представление о необходимости
соответствующей реакции.
Рис. 1. Диаграмма классов MVC
Важно отметить, что как представление так и контроллер зависят от
модели. Однако модель не зависит ни от представления, ни от контроллера.
Это одно из ключевых достоинств подобного разделения. Оно позволяет
строить модель независимо от визуального представления. Разделение
представления и контроллера вторично для многих настольных приложений,
однако в Web-приложениях такое разделения ярко выражено.
Итак, как же применить данный шаблон при разработке Web-приложения?
3. Некоторые особенности Web-приложений
Web-приложения обладают следующими (на мой взгляд важными)
особенностями:
- Использование протокола HTTP не позволяет «помнить состояния».
- Традиционно состояния запоминаются с использованием сессий и/или
механизма Cookies либо просто «катаются» между клиентом и сервером в
скрытых полях форм.
- Клиент и сервер «общаются» между собой с помощью запросов и
ответов (request/response) при этом одновременно с запросом
передаются параметры запроса (PostBack Data).
- Действия (команды) инициируются параметрами запросов. Также с
помощью параметров передаются необходимые данные.
- Как правило web-приложения используют СУБД, причем до недавнего
времени удачных механизмов абстрагирования от данных не было.
- Наиболее частый способ разделения интерфейса и бизнес-логики –
использование механизма шаблонов. Более продвинутые технологии
предлагают объектную модель + шаблоны, вариации с XML как
промежуточным форматом.
Список можно было бы продолжить.
К сожалению большинство перечисленных особенностей одновременно
являются недостатками.
4. «Пережитки» прошлого?
Чаще всего программистам для решения их проблем предлагались решения,
в общем-то продиктованные самим HTTP протоколом. Некоторые из них
являются обязательными в модели каждого Web-приложения. В первую очередь
это Request (+parameters, +cookies), Response и
Redirect (серверный либо того хуже клиентский). Причем пользоваться
ими следовало обязательно – без них Web-приложение просто не построить.
А можно ли абстрагироваться от их прямого использования? Конечно же они
остануться, но непосредственно в логике работы приложения они
участвовать не должны.
В ASP.NET есть механизм событий, причем использовать его можно так же
(почти) как и в настольных приложениях. Его то и следует применять
вместо параметров, выполняющих роль команд. Для многих Web-разработчиков
события покажутся чем-то особенным, однако, поверьте, в этом мощном
средстве стоит разобраться.
Далее следуют Session и Application. Реализованные в
разных технологиях по-разному, они появились как логичное воплощение
желания облагородить Web-приложения. Без них можно написать
полнофункциональное приложение, но какой ценой! Причем, если Session
бесспорно следует использовать, то Application все чаще
рассматривается как рудимент.
5. События и свойства и/или параметры?
Практически все элементы управления (Controls) в ASP.NET могут
генерировать те или иные события. Например, LinkButton генерирует
события Click и Command.
При этом аргументами, скажем, для Command может быть любой
объект (например, строка с текстом команды, подлежащей выполнению).
Таким образом, нет необходимости напрямую использовать для этих целей
PostBack параметры.
Теперь о данных. Элемент управления, если это предусмотрено его
функциональностью, как объект позволяет обращаться к своим свойствам.
Получается, что и данные передавать (явно преобразовывать и
анализировать на сервере) как параметры (PostBack) также сложнее, чем
обратиться непосредственно к соответствующему элементу управления,
хранящему в себе эти данные.
Из вышесказанного следует, что параметры запроса теперь не должны
волновать прграммиста. Есть объекты, их свойства. Есть события. Можно
строить приложение. Исключение, наверное, может составить лишь момент
инициализации Web-формы/страницы, когда все-таки параметры запроса более
предпочтительны (например, при обращении из другого приложения).
Для хранения данных/состояния между запросами обычно используют
сессию (Session), ViewState или СУБД.
6. При чем тут MVC?
Попробуем представить себе, как же можно применить MVC при построении
ASP.NET Web-приложения.
Каждая ASP.NET страница (ASPX) или пользовательский элемент
управления (ASCX) состоит из двух частей (файлов): Design и CodeBehind.
В первом приближении Design это представление (View), а CodeBehind это
контроллер (Controller). На самом деле они “родственники” и первое
наследуется от второго. В идеале вы помещаете элементы управления на
Design и пишете обработчики событий в CodeBehind. На практике же часто
Design содержит только каркас для View (например, с placeholders), а
элементы управления порождаются и добавляются из CodeBehind одновременно
с регистрацией обработчиков событий. Т.е. получаем совмещенный
View-Controller, который динамически строит страницу и соответствующие
обработчики.
Хочется подчеркнуть, что хдесь имеются в виду сложные элементы
управления (назовем их модулями приложения), каждый из которых должен
иметь модель и реализует в себе свой View-Controller, порождая и
обрабатывая свои элементы управления.
Что же будеть представлять из себя модель целого приложения?
Каждому сложному элементу управления будет соответствовать класс
модели, которая собственно и реализует функциональность этого элемента.
Функциональность включает в себя бизнес-логику, работу с СУБД,
взаимодействие с другими моделями и т.д.
Модель представляет собой обычные классы.
// Модель приложения
public class AppModel : GeneralApplicationModel
{
protected SqlConnection _modelSQLConnection;// одно для модели
protected ModelState _currentState; // текущее состояние модели
protected string _currentCommand; // текущая выполняемая команда
...
protected MainMenuModel _menuModel; // модель главного меню
protected HeaderBlockModel _hdrModel; // модель блока заголовка
protected SecurityModel _securityModel; // модель безопасности
...
protected Hashtable _allModels; // коллекция всех моделей
...
// логика приложения
public void PerformApplicationLogics(string command)
{
...
}
...
// пример внутримодельного события
// Обработчик события смены языка интерфейса для системы
public void OnLanguageChange(object sender, System.EventArgs e)
{
//код нового языка интерфейса
int _newLCID = ((LanguageChangeEventArgs)e).LCID;
foreach(GeneralContentModel theModel in AllModels)
{
if (theModel is IMultilingual)
theModel.RepopulateContent(_newLCID);
}
}
}
// Модель главного меню системы. Не содержит данных о представлении
// этого меню, но отвечает за его содержимое и функциональность
// GeneralContentModel – родительский класс реализующий общую для всех
// моделей функциональность
// IMultilingual – интерфейс, реализуемый классами, имеющими
// многоязыковую поддержку.
public class MainMenuModel : GeneralContentModel, IMultilingual
{
protected MainMenuItem[] _menuItems; // пункты меню
public void RepopulateContent(int newLCID) // IMultilingual
{
// сменить текстовые названия пунктов меню в соответствии с
// новым языком общения (newLCID)
...
// после этого содержимое пунктов меню на текущем языке до
// следующей смены языка (или конца сессии) будет
// извлекаться только из памяти, а не из СУБД
}
}
// Модель пункта меню.
public class MainMenuItem
{
protected string _itemText; // текст пункта меню
protected CommandType _itemCommand; // команда пункта меню
...
}
Объект самого верхнего уровня, содержащий в себе структуры данных с
моделями сложных элементов управления, порождается при старте сессии
(Session), записывается в нее и “живет” там до окончания сессии.
protected void Session_Start(Object sender, EventArgs e)
{
Session.Add("Model", new AppModel());
...
}
Таким образом просматривается аналогия с обычным настольным
приложением. Пользователь с помощью обозревателя интернет обращается к
Web-приложению в первый раз и для него порождается экземпляр модели
этого приложения.
Преимущества такого подхода:
- Модель доступна на протяжении всей сессии.
- Модель всегда готова к действию. Ее время реакции минимально.
- Модель может хранить часто используемые данные и свое состояние
(здесь некоторое перекрытие фнкциональности с сессией, но на более
высоком уровне).
- Элементы управления получаются очень легковесными компонентами,
так как не содержат бизнес логики и работы с СУБД. Они – лишь
интерфейс. Каждый элемент управления имеет доступ к состоянию своей
модели и модели приложения в целом.
Недостаток:
- До окончания сессии модель будет занимать память.
На самом деле недостаток являетса весьма существенным. Ведь ASP.NET
как раз и проповедует идею полной динамики т.е. не хранить объекты в
памяти а динамически создавать их + кэширование. Всегда. Такой подход
позволяет обслужить максимальное количество пользователей. Т.е. ресурсы
достаются всем поровну. Если же хранить объекты на сервере, мы получаем
не количество, а качество – экземпляр приложения, созданный для
конкретного пользователя и работающий для него. Однако ничто не мешает
искать золотую середину, используя кэширование, динамически изменяемую
модель (с созданием только нужных и удалением ненужных частей), можно
оптимизировать управление сессией, и в конце концов можно использовать
динамически создаваемую при каждом запросе модель с возвратом в прежнее
состояние. Не следует также забывать о ViewState, «катающемся» между
клиентом и сервером при каждом запросе. Существуют некоторые (именитые)
сайты, где одно обращение клиента обходится в 200! и более килобайт
трафика (при реальном количестве полезной текстовой информации 2Кб)! +
еще графика. Уж не лучше ли в таких случаях, чтобы эти данные спокойно
лежали на сервере?
Предполагается, что приложение будет занимать примерно одинаковый
объем памяти при разных реализациях (с моделью или без нее). Ведь
приложение должно выполнить свою работу, а по объему это тот же код.
Что касается данных, то при использовании MVC в Web-приложениях
рекомендуется разделить все используемые данные на 3 группы:
- Часто используемые, небольшие по объему данные. Это могут быть,
например, локализованные текстовые блоки (пункты меню – см. пример,
типовые кнопки и ссылки). Модель вычитывает такой блок из базы
только однажды при создании соответствующего элемента управления и
хранит на протяжении всей жизни модели данного элемента управления.
Обновление этих данных происходит по событию. Например, событие
«смена языка» инициирует в модели обновление текстов всех меню на
новом языке.
- Актуальные данные. Каждое обращение за этими данными к модели
порождает соответствующий запрос к базе данных и генерацию
соответствующих структур в модели. Эти данные чаще всего
нежелательно кэшировать.
- Служебные и константные данные. В эту группу включены структуры
данных, отражающих состояние модели, а также данные, традиционно
хранящиеся в сессии (например, userID, role, утилитные объекты).
Вернемся к аналогии с настольными приложениями. Так как представление
(View) строится динамически, необходимость в большом количестве страниц
исчезает.
В идеальном случае приложение для клиента будет состоять всего из
одной страницы. Также рекомендуется использовать шаблон Page Controller
или шаблон Front Controller описанные в [1].
Для описанного выше примера модели CodeBehind будет выглядеть
примерно следующим образом.
// Codebehind страницы/формы
public class HomePageForm : BasePage //в конечном итоге от System.Web.UI.Page
{
...
// Web Control отвечающий за представление меню
protected MainMenu ctrlMainMenu;
// Web Control, генерирующий событие “Смена языка”
protected LanguageMenu ctrlLanguage;
// Доступ к Модели приложения
AppModel _model = (AppModel)Session["Model"];
...
private void Page_Load(object sender, System.EventArgs e)
{
// Динамическая загрузка элементов управления
...
// Загрузка может быть условной,
// в зависимости от состояния модели.
if (_model.CurrentState.NeedMainMenu)
{
ctrlMainMenu =
(MainMenu)LoadControl("~/DesktopModules/MainMenu.ascx");
MainMenuPane.Controls.Add(ctrlMainMenu);
}
...
SubscribeEvents();
}
// подписывание на события всех "заинтересованных"
private void SubscribeEvents()
{
ctrlLanguage.LanguageChange +=
new System.EventHandler(_model.OnLanguageChange);
...
}
}
ASPX или ASCX файл будет выглядеть примерно следующим образом:
...
<table>
<tr>
<td colspan="2" id="LanguageMenuPane" runat="server"></td>
</tr>
<tr>
<td id="MainMenuPane" runat="server" ></td>
<td id="SubMenuPane" runat="server" ></td>
</tr>
</table>
...
Замечание: Для простого Web-приложения модель становится моделью
интерфейса и использование MVC теряет смысл.
7. Где же «грабли»?
Ранее говорилось, что приложение, написанное с использованием ASP.NET
ведет себя ПОЧТИ как и настольное приложение. В этом кроются некоторые
трудности.
Во-первых, каждый элемент управления имеет свой жизненный цикл.
Во-вторых, каждый postBack инициирует этот цикл, который делится на
несколько фаз [2].
Для простоты сгруппируем и рассмотрим по порядку основные:
- Инициализация.
- Восстановление предыдущего состояния.
- Генерация событий
- Выполнение полезной работы
- Формирование и выдача элемента управления клиенту
Каждая фаза выполняется практически одновременно для всех элементов.
Вывод: динамические элементы управления всегда должны формироваться
на стадии инициализации. Это гарантирует их корректное восстановление и
последующую генерацию событий.
Как быть, если динамически создаваемые элементы управления зависят от
событий? Ведь получается, что некоторые части инициализации должны будут
выполняться после генерации событий. Для решения этой проблемы следует
применять двухшаговый алгоритм (Action->View). Суть его заключается в
том, что на первом шаге (при генерации событий) обработчик события:
- Изменяет состояние модели в соответствии с полученными данными.
На этом этапе жизненного цикла все элементы управления уже
восстановили свое состояние и содержат корректные данные.
- Выполняет Server.Transfer (HttpServerUtility.Transfer). Причем в
случае, когда сайт состоит из одной страницы, Transfer выполняется
«на себя». На этом шаге фаза инициализации пройдет в соответствии с
новым состоянием модели. Необходимо отметить, что следует
использовать именно Transfer, а не Redirect с целью повышения
производительности [3].
Ниже приведен пример, реализующий данную концепцию.
// CodeBehind базового класса элемента управления
public class BaseModuleClass : System.Web.UI.UserControl
{
protected void Page_Load(object sender, System.EventArgs e)
{
...
InitForm();
...
}
protected void Pre_Render(object sender, System.EventArgs e)
{
...
PopulateForm();
...
}
protected virtual void InitForm()
{
// Переопределенный, этот метод должен реализовывать все операции по
// динамическому созданию и инициализации элементов управления включая
// подписывание на события.
// После него "выстреливают" все события, которые должны изменить
// состояние модели
}
protected virtual void PopulateForm()
{
// Переопределенный, этот метод предназначен для выполнения полезной
// работы. События уже отработали и модель изменила свое состояние.
// Если новое состояние модели не требует изменения View, идет
// заполнение элементов управления контентом (для примера с меню –
// текстом на текущем языке для пунктов меню).
// Причем контентом именно из модели.
//
// Если новое состояние модели требует нового отображения (View),
// выполняется Server.Transfer(“default.aspx”)
// Server.Transfer, однако, может быть выполнен и раньше (например в
// обработчике события)
}
}
8. Итак, стоит ли «городить огород»?
ASP.NET - уникальная Web-технология. Вы можете использовать ее по
аналогии с любыми из уже существующих. Например, мне встречались способы
написания Web-приложений по аналогии с ASP, PHP, JSP+
JavaBeans+Servlets. Все они использовали потенциал платформы
приблизительно наполовину. Нескольких «типовых» конструкций обычно
достаточно, ведь платформа настолько избыточна по функциональности.
Повторное использование кода здесь делает медвежью услугу – стиль
написания вместе с целыми библиотеками кочует из проекта в проект.
Собственно, причиной написания данной статьи и стало желание
поделиться идеей. Начав очередной проект я поймал себя на мысли, что
дальше подражания чему-то старому ничего не движется. Четыре подряд(!)
перепроектирования / переделки весьма серьезного движка именно с целью
выработать и обкатать идеологию и привели к переосмыслению идей,
заложенных в ASP.NET. Статья не содержит полного руководства, однако,
надеюсь, поможет многим сориентироваться.
Где можно наверняка реализовать данную концепцию?
Прежде всего это немассовые специализированные системы. Например,
web-приложение для агентов по недвижимости, турагентов, обучающая
система, система для групповой работы, любая корпоративная система. А
также везде где, выполняется аутентификация и пользователю следует
доверять и предоставлять максимум ресурсов. Хочется еще раз подчеркнуть,
что речь идет о Web-приложениях, содержащих сложную логику. Применение
описанного подхода при решении простых задач экономически не оправдано.
9. Выводы
Итак, при достижении приложением определенного уровня сложности (для
ориентации - как минимум сложности обычного «web-форума»), появляются
следующие преимущества.
- Уменьшаются зависимости между частями кода.
- Уменьшается необходимость повторения кода
- Более четко разделяются обязанности между разработчиками
- Упрощается тестирование, которое можно проводить независимо для
модели, контроллера и представления.
- Теперь можно с легкостью определить, какова трудоемкость
переделки интерфейса приложения (а это случается с большинством
Web-приложений, причем со многими еще до окончания разработки). Речь
идет именно о переделке интерфейса (look-n-feel), а не о смене
внешнего вида (style).
- Можно ли вести разработку Web-приложений применяя к ним подход и
методики, аналогичные desktop приложениям? Да. Это относится также к
тестированию.
- Полезная модель всегда может обрести новую жизнь как Web-,
Desktop- приложение или как сервис.
10. Заключение
Цель данной статьи – обратить внимание Web-разработчиков на богатство
функциональности, предоставляемое технологией ASP.NET, и, в какой-то
степени, спровоцировать их к пересмотру устоявшихся принципов создания
Web-приложений. Тех же, кто регулярно «изобретает велосипед», хотелось
бы натолкнуть на мысль, что есть большое количество отлаженных шаблонов
и подходов к разработке тех или иных классов приложений (Patterns &
Practices), которыми без сомнения следует пользоваться.
Литература
[1].
Microsoft patterns & practices Patterns , MSDN, Microsoft Patterns &
Practices, © 2003 Microsoft Corporation
[2].
Разработка серверных элементов управления ASP.NET ,
Шатохина
Н.А. , © 2003
[3].
ASP.NET Optimization, MSDN, .NET Framework Developer’s Guide, © 2003
Microsoft Corporation.