четверг, 26 ноября 2009 г.

10 простых правил, чтобы лучше писать приложения на ASP.NET

При создании приложений на ASP.NET, мы, как правило, тратим множество усилий на проектирование и изоляцию различных слоев приложения, с упором на предметную область и тому подобное. Однако, как только мы приближаемся к программированию пользовательского интерфейса, код становится более грязным. Я могу объяснить это тем, что чем ближе мы к пользовательскому интерфейсу, тем меньше шансов того, что код используется повторно, или, по крайней мере, предназначен для многократного использования. Несмотря на то, что это может быть правдой (что код не предназначен для повторного использования), UI-код должен быть чистыми и легким для сопровождения.

Код UI должен использовать такие принципы ООП, как наследование и инкапсуляция, ровно как код любого другого слоя. Кроме того, некоторые принципы SOLID, могут и должны быть применены при разработке наших юзер-контролов и веб-страниц. Так как ASP.NET WebForms по-прежнему широко используется, и я полагаю, что так продлится еще долгое время, в этой статье, я предлагаю тот набор правил для программирования и проектирования, который я использую в своих ASP.NET проектах. Надеюсь, что следующие правила помогут вам сделать ваш UI код более чистым и и облегчат его поддержку в дальнейшем.

1. Используйте собственный базовый класс для классов пользовательского интерфейса


Начинайте проект со создания крайней мере двух базовых классов BasePage и BaseUserControl и наследовать от них все ваши веб-формы и юзер-контролы. Это считается хорошим стилем. Причина в том, что у вас непременно возникнет необходимость реализовать общий код для всех или части ваших веб-форм или юзер-контролов.
К примеру, все страницы вашего сайта должны иметь название и, в дальнейшем, некоторым мета-теги. Добавление свойства PageID, а также метода, который вернет название страницы и ее мета-теги будет довольно легко достигнуть при помощи наследования, а поддерживать код, очевидно, станет намного легче.
Другим распространенным случаем является необходимость показать / скрыть элементы веб-формы, в зависимости от определенного условия. Добавив виртуальный метод в базовый класс, который вызывается при возникновении события OnPreRender, вы сможете реализовать механизм скрытия и отображения элементов веб-формы. И весь этот код будет удобно расположен в одном методе и работать для всех классов-наследников, пока вы не переопределите его. Следующий код демонстрирует все вышесказанное, используя дизайн-паттерн "Шаблонный метод"

Пример:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Specialized;
using System.Data;
public class BasePage : System.Web.UI.Page
{
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
DoEnableDisableControls();
SetTitleFromCMS();
SetMetaTagsFromCMS();
}
public string PageID
{
get;
set;
}
/// <summary>
/// Overwrite this method to enable or disable controls
/// depending on the state of the page.
/// </summary>
protected virtual void DoEnableDisableControls()
{
}

private void SetTitleFromCMS()
{
if (String.IsNullOrEmpty(PageID))
return;
//Queries the CMS for the page title
this.Title = "Whatever we got from the CMS";
}
private void SetMetaTagsFromCMS()
{
//Queries the CMS for the MetaTags
//add the meta tags
}
}


Странице, которая наследуется от базового класса, будет просто необходимо установить свойство PageID – и она будет иметь заголовок и мета-теги (обратите внимание на возможность задать PageID непосредственно в директивах .aspx файла страницы).

<% @ Page Language = "C #" PageID = "DefaultPage" AutoEventWireup = "True" CodeBehind = "Default.aspx.cs" Inherits = "% ExtRefWebApp._Default">


Использование такого базового класса, позволит вам внедрить общий код в любое время процесса разработки, и это будет очень полезно при будущей доработке всех страниц-наследников.

2. Скрывайте переменные сессии


Ниже перечислены причины, почему я никогда не использую переменные сессии непосредственно в коде, а инкапсулирую в отдельные классы:

  • Переменные сессии широко используется в веб-приложениях. Они очень полезны, но мы не должны забывать, что переменная Session, с точки зрения ООП – это ничто иное, как глобальная переменная.
  • При отладке или поддержке веб-приложений, мы часто сталкиваемся с участками кода, которые используют значения, хранящиеся в Session, и нам необходимо проверить, где эти значения были установлены. И это не легко выяснить.
  • Session может содержать любой тип. Если мы заменим тип значения, хранящегося в определенном ключе Session, нам придется проверить весь код и изменить все приведения типов (мы делаем это с помощью поиска/замены текста в коде).
  • Конкретного ключа Session может не существовать, либо он может иметь значение null. Не существует способа для того, чтобы получить по умолчанию для несуществующего ключа Session и, таким образом, всякий раз, когда мы используем переменную сессии, мы должны проверить ее значение (на null) перед использованием.

Лучший способ избежать этих проблем – создать набор классов, по средствам установки свойств которых мы и будем изменять значения Session. При помощи функции Visual Studio "Find all references"/"Find Usages", из контекстного меню при клике на сеттере свойства, вы найдете все объекты, которые устанавливают переменную сеанса. С другой стороны, если мы изменим тип значения в переменной Session, то код, использующий старый тип просто выдаст ошибку компиляции, следовательно, эти ошибки будет очень легко найти и сделать все изменения или оценить объем работы, необходимой, чтобы сделать изменения. Это ужесточает контроль типов и делает наш код более безопасным.
Доступ к значениям Session должен быть инкапсулирован в разных классах, и если их будет слишком много, то, желательно, чтобы они принадлежали одному пространству имен и находились в одной папке, это облегчит их поддержку.

Можно еще много всего сказать об использовании Session. Например, если сессии используются в чистом виде (без обвертки в специальный класс) не только в слоях пользовательского интерфейса, но и слоях бизнес-логики приложения; то уже излишне говорить, что в этом случае мы нарушим принцип инкапсуляции и принцип изоляции слоев приложения. Ведь теперь слой бизнес-логики зависит от HTTP контекста (мы больше не можем повторно использовать его в не HTTP приложениях).

3. Не создавайте глубокой иерархии UI-контролов


Пользовательские элементы управления в ASP.NET являются очень хорошим средством для повторного использования кода пользовательского интерфейса. Однако, когда мы начинаем вставлять одни юзер-конролы в другие, то, в конце концов мы получаем бесконечное дерево вложенных друг в друга элементов управления . Это превращает поддержку приложения в настоящий ад.

Обычно, это хорошая привычка ограничить дерево максимум на 2 или 3 уровня. Страница содержит элементы управления, которые могут содержать другие элементы управления. Но внутренние элементы должны содержать только базовые элементы управления .NET Framework (или элементы управления веб-сервером). И даже на одном уровне, мы должны ограничивать число юзер-контролов, включенных в родительский юзер-контрол. В том смысле, что создание одного большого юзер-контрола, который содержит множество внутренних юзер-контролов – это не всегда хорошая идея. Имейте в виду, что сама страница может содержать столько контролов, сколько вы пожелаете: я заметил, что очень часто разработчики не используют базовые контролы ASP.NET, а встраивают все в пользовательский Сверх-элемент управления, который в свою очередь, содержит дочерние элементы. Зачем нам добавлять этот уровень косвенности и сложности? Страница – это прекрасный контейнер для элементов управления.

4. Не все должно быть включено в отдельный пользовательский элемент управления


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

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

Например, крупную форму, которая содержит несколько разделов, значения которых которые могут зависеть друг от друга. Она будет реализована эффективней, если мы выделим эти разделы в отдельные контролы и заставим их обмениваться данными по средствам событий так, как описано в следующем пункте.

5. Используйте события для обмена данными между юзер-контролами и их контейнерами


Использование свойств пользовательских элементов управления, чтобы сообщить о своем состоянии своему контейнеру – это типичная схема, которую мы видим в веб-разработке. В большинстве случаев разработчики устанавливают переменную сессии, когда что-то случается в пользовательском элементе управления, и контейнер (будь то страница или другой пользовательский элемент управления) проверяет значение переменной сессии для того, чтобы определить, действительно ли что-то случилось или нет. Это обычно делается в событие "OnLoad". Наверное, вы заметили, что очень быстро, мы, в итоге, получаем довольно беспорядочный код в этом обработчике событий.
Во-первых, мы должны гарантировать, что код, который устанавливает переменную сессии выполняется перед кодом, который проверяет ее значение. Кроме того, мы должны гарантировать, что значение переменной сбрасывается: в противном случае, в следующий раз, когда мы попадаем на ту же страницу, мы можем спровоцировать событие, без всяких на то причин.
Один элегантный способ уведомить об изменение статуса одного элемента другому – это создать событие, на которое можно будет подписаться (прим.: public event в C#). Есть много преимуществ такого подхода:

  • Чистый код, который реагирует на конкретные события с внутренних контролов,
  • Один и тот же механизм может быть использован для стольких контролов, сколько мы пожелаем,
  • Мы будем уверены в том, что наш контрол будет извещен только в том случае, если событие действительно произошло; в то время как использование переменных сессии добавляет неопределенности, поскольку мы не знаем, какой код выполняется первым: код, который установил значение Session, или код, который проверяет его.
  • Мы не должны заботиться о переменной сессии и сбрасывать его после событий, поскольку мы больше не используем их.
  • Мы не используем значения Session просто для того, чтобы поставить/считать флаг (а это экономия памяти и уважение основных принципов ООП).


6. Избегайте встраивания C#/VB кода в файлах ASPX


Лучшее место, чтобы добавить программный код в веб-формы – это code-behind файл. Внедрение кода в файлы ASPX делает его грязным и очень трудно поддерживаемым, как в старые времена ASP.

7. Используйте декларативный стиль программирования как можно чаще


Если вам необходимо настроить пользовательский или любой другой серверный элемент управления – сделайте это по средствам свойств в ASPX-файле вместо code-behind. Конечно же, бывают ситуации, когда у нас нет выбора, где настроить элемент управления (условные значения, например), но, в общем, это делает код HTML более четким и облегчает его поддержку, поскольку мы знаем многое о контроле, просто взглянув на один ASPX файл (и нет необходимости смотреть в code-behind файл).

8. Будьте осторожны при использовании статических переменных


Статические члены являются общими и сохраняют свое значение до тех пор, пока работает процесс ASP.NET. Это означает, если вы используете статические члены, вы потребляете память, которая не будет освобождена, пока вы не сделаете это явно.

Один пример я видел в прошлом, проект хранил пользовательскую информацию в виде статического словаря (между прочим, это может быть любой тип коллекции). При нагрузочном тестировании, использование памяти приложения выросло до 12 ГБ на пару сотен пользователей. Так как словарь хранился в статической переменной, ее размер увеличивался при добавлении новых пользователей и, в конце концов, мы получили исключение "Out of memory".

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

9. Рассматривайте производительность с точки зрения клиента


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

Существуют различные средства и способы, чтобы сделать производительность страницы лучше на клиентской стороне (хорошей отправной точкой, будет эта статья: http://www.codeproject.com/KB/aspnet/PeformanceAspnet.aspx).

В коде на серверной стороне, мы не должны допускать чрезмерного использования ViewState. С одной стороны, хранение дерева объектов в ViewState может показаться полезной и более эффективным техникой, чем сохранять его в сессии, ведь объект не будет висеть на неопределенное время в памяти. Но, эффективность будет еще хуже, поскольку на самом деле ваше дерево объектов будет сериализовано и передано как часть ответа в браузер клиента. И когда клиент отправит форму, сериализованный объект отправляется обратно на сервер.

Другой проблемой является количество Javascript файлов, которые загружаются вместе со страницей. Мы должны объединить их с помощью ScriptManager (см. http://bellouti.wordpress.com/2008/09/14/combining-javascript-files-with-ajax-toolkit-library/ ).

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

10. Используйте кэширование, где это только возможно


Пользовательские элементы управления можно кэшировать с помощью директивы OutputCache. При создании пользовательских элементов управления, которые должны показываться содержание, взятое из БД или внешних источников, следует рассмотреть возможность использования этой директивы.
Мы также можем кэшировать данные в статических членах или пользования объектами кэширования EnterpriseLibrary. Если мы сделаем это, мы должны гарантировать, что мы создаем кэш в форме, ближайшей к той, что будет выводиться в интерфейсе пользователя. Например, если вы получаете XML из базы данных и трансформируете его по правилам XSLT в HTML, который и будет отображаться, то кэшировать следует результирующий HTML, а не XML. Кэширование HTML позволит избежать повторных преобразований XML в следующий раз, когда будет запрошена страница.



Автор статьи: Samir Bellouti
Оригинал: 10 simple rules to write better ASP.NET applications
Перевод: Дмитрий Жарий

6 коммент.:

Ilya Barkov комментирует...

Дима, слушай, а не посоветуешь какую-то хорошую (и желательно русскоязычную) литературу про кеширование в ASP.NET-приложениях? А то я этой темы касался не очень близко, а чувствую, что скоро может весьма пригодиться.

DmytroZ комментирует...

Чего-то конкретного по кэшированию посоветовать не готов, но могу досказать, что в книге Мэтью Мак-Дональда «Microsoft ASP.NET 2.0 с примерами на C# 2005 для профессионалов» тема стандартных подходов кэширования раскрыта довольно хорошо (для начала).

Анонимный комментирует...

Может это конечно не слишком относится к asp.net, но рекомендую по возможности использовать Stored Procedures при работе с SQL Server и переложить часть логики на него. Код станет намного чище.

Анонимный комментирует...

Вот у нас в приложении все на хранилках.
Так вот при добавление нового поля в таблицу
приходиться менять с добрый десяток процедур
и не где не чего не потерять.
A дебаг процедуры в 400 строк полный ад.
SP хороший инструмент но все на них строить
не эффективно в "Эпоху Кризиса" :)
Теперь только ORM даже при всей моей ненависти к nhibernate

Анонимный комментирует...

ХП используя для обновления и не слишком сложного ада. Для вставки и получения информации LINQ2SQL.

dburchik комментирует...

Спасибо, статья полезная. Правда она больше подходит для начинающих asp.net разработчиков с опытом до 1-2 лет. Для остальных послужит еще раз напоминанием о некоторых принципах разработки. Те, кто знаком с asp.net несколько лет уже успели каждый из данных пунктов опробовать на себе как минимум с десяток раз :) Насчет переноса бизнес-логики в ХП не соглашусь и посоветую почитать к примеру следующую статью http://habrahabr.ru/blogs/refactoring/65432/

Отправить комментарий

 

.NET ate my MOSK;. Powered By Blogger © 2009 Bombeli | Theme Design: ooruc