Построение ASP.NET приложений

Некоторые фреймворки настаивают на создании и управлении жизненным циклом написанных нами классов. Самый популярный фреймворк – ASP.NET (Web Forms, в противоположность MVC).

Примечание

Некоторые другие фреймворки, разделяющие эту особенность, – это Microsoft Management Console (MMC), управляемый SDK, и такая недавняя разработка, как PowerShell.

Наиболее очевидным симптомом таких фреймворков является то, что, чтобы им подойти, наши классы должны обладать конструктором по умолчанию. В ASP.NET, например, любой реализуемый нами класс Page должен обладать непараметризованным конструктором. Мы не можем использовать Constructor Injection в рамках этих фреймворков, поэтому давайте рассмотрим наши возможности.

ASP.NET композиция

Паттерн Constructor Injection был бы предпочтительным, поскольку он позволял бы удостовериться, что наши классы Page будут правильно инициализироваться совместно с их зависимостями. Так как это невозможно, мы должны выбирать между следующими альтернативными вариантами:

  • Перемещать и дублировать наши Composition Root'ы в пределах каждого класса Page.
  • Использовать Service Locator для того, чтобы преобразовать все зависимости в пределах каждого класса Page.

Тем не менее, не забывайте о том, что Service Locator – это анти-паттерн, поэтому такой вариант не желателен. Наилучшая альтернатива – пойти на компромисс в вопросе расположения нашего Composition Root.

В идеале, мы бы предпочли сценарий, продемонстрированный на рисунке 7-13, при котором мы имеем всего один Composition Root для приложения, но в ASP.NET это невозможно, поскольку мы не можем компоновать экземпляры Page извне. Другими словами, фреймворк Web Forms вынуждает нас компоновать приложение в пределах каждого Page.

Рисунок 7-13: В идеальном мире нам хотелось бы уметь компоновать объекты Page из Composition Root приложения. При получении запроса мы должны уметь использовать определенную конфигурацию зависимостей для того, чтобы скомпоновать соответствующий объект Page. Тем не менее, это невозможно, поскольку ASP.NET управляет жизненным циклом объектов Page от нашего имени.

Примечание

До настоящего времени я говорил только об объектах Page, но ASP.NET требует, чтобы большинство объектов обладали конструкторами по умолчанию в случае, если мы хотим использовать фреймворк. Еще одним примером является Object Data Sources. Обсуждение, имеющее место в данном разделе, в равной степени хорошо применяется ко всем остальным типам, которые должны обладать конструктором по умолчанию.

Чтобы обратиться к этой проблеме, мы должны подвергнуть риску наши идеалы, но компромисс в вопросе расположения наших Composition Root'ов кажется мне более безопасным, чем позволить Service Locator вступить в игру.

По существу мы преобразуем каждый Page в Composition Root, как это продемонстрировано на рисунке 7-14. Принцип единичной ответственности напоминает нам о том, что каждый класс должен иметь только одну ответственность; теперь, когда мы используем Page для того, чтобы скомпоновать все необходимые зависимости, мы должны делегировать ответственность за реализацию реализатору (implementer). Такой подход эффективно преобразует Page в humble-объект ("скромный" объект), раскрывая другие члены, такие, как обработчики события нажатия кнопки только для того, чтобы делегировать полномочия реализатору преобразованного Page.

Рисунок 7-14: В ASP.NET мы можем использовать точку входа в приложение (global.asax) для того, чтобы конфигурировать зависимости, но потом до того, как мы сможем продолжить композицию, нам придется ждать, пока фреймворк не создаст новый объект Page. В пределах каждого Page мы можем использовать сконфигурированные зависимости для того, чтобы скомпоновать реализатор, который реализует все поведение класса Page.

Различие между вариантом перемещения Composition Root в каждый класс и вариантом использования Service Locator – трудно уловимо. Разница заключается в том, что благодаря Service Locator мы можем преобразовывать зависимости каждого класса Page в индивидуальном порядке, и использовать их напрямую в пределах класса Page. Как обычно бывает с Service Locator, он склонен к размытию фокуса класса. Кроме того, довольно заманчиво сохранить контейнер и использовать его для того, чтобы преобразовать остальные зависимости как надо.

Чтобы противодействовать этой тенденции, важно использовать наш контейнер только для преобразования реализатора, а затем – забыть о нем. Это позволяет нам руководствоваться подходящими DI-паттернами (например, Constructor Injection) для остальной части кода приложения.

Несмотря на то, что это только теория, вы расслабитесь, услышав, что это легко реализовать. Лучше всего это иллюстрируется на примере.

Пример: подключение CampaignPresenter

Шаблонное приложение Commerce, которое вы знаете и любите, поддерживает такие возможности, как скидки на товары и список рекомендуемых товаров, но до настоящего момента вы не обеспечивали потребителей приложением, которое управляло бы этими аспектами. В данном примере мы рассмотрим, как компоновать ASP.NET приложение (продемонстрировано на рисунке 7-15), которое позволяет потребителю обновлять данные об акциях для товара.

Рисунок 7-15: Приложение CampaignManagement позволяет промышленным потребителям редактировать данные об акциях (Featured (Рекомендуемые) и Discount Price (Цена со скидкой)) для товара. Это ASP.NET приложение, построенное с элементом управления GridView, привязанным к элементу управления ObjectDataSource.

Говоря простыми словами, это приложение состоит из единственного элемента управления, привязанного к ObjectDataSource. Источник данных – это класс конкретного приложения, который делегирует свое поведение доменной модели и, в конечном счете, передает его в библиотеку доступа к данным, которая хранит данные в базе данных SQL Server.

Вы все еще можете использовать global.asax для конфигурирования зависимостей, но вы должны отложить компоновку приложения до тех пор, пока не будут созданы Page и его ObjectDataSource. Конфигурирование зависимостей схоже с предыдущими примерами.

Конфигурирование зависимостей в ASP.NET

В ASP.NET точкой входа в приложение является файл global.asax, и, несмотря на то, что вы ничего не можете компоновать в этой точке, вы можете создать свой mise en place, подготавливая все для запуска приложения:

protected void Application_Start(object sender, EventArgs e)
{
	this.Application["container"] =
		new CampaignContainer();
}

Единственное, что вы здесь делаете, – создаете ваш контейнер и сохраняете его в Application Context, поэтому вы можете использовать его, когда вам это нужно. Это позволяет вам совместно использовать контейнер в рамках отдельных веб-запросов, которые являются существенными, если вам нужно сохранить некоторые зависимости на время жизненного цикла процесса (более подробно о жизненных циклах мы поговорим в главе 8).

Примечание

Как и во всех остальных примерах данной главы, я использую Poor Man's DI для того, чтобы продемонстрировать основные рассматриваемые принципы. CampaignContainer – это пользовательский класс, созданный специально для этого примера, но вы можете легко заменить его выбранным вами DI-контейнером.

Большинство различных Page и объектов источников данных могут совместно использовать один и тот же контейнер посредством обращения к Application Context. Тем не менее, этот подход несет за собой опасность неправильного использования его в качестве Service Locator, поскольку любой класс может потенциально получить доступ к Application Context. Таким образом, важно делегировать реализацию классам, которые не могут получить доступ к Application Context. На практике это означает делегирование полномочий классам, реализованным в других отдельных библиотеках, которые не ссылаются на ASP.NET.

Примечание

Мы можем также продолжить свой путь слегка дисциплинированно, сдерживая себя от обращения к Application Context, за исключением реализации Composition Root. Это может хорошо подходить в тех случаях, когда все разработчики имеют опыт в написании слабо связанного кода; но если мы думаем, что некоторые члены команды могут не до конца понимать рассматриваемую проблему, мы можем удачно защитить код посредством использования отдельных библиотек. Раздел 6.5 описывает, как это сделать.

В текущем примере вы будете делегировать всю реализацию отдельной библиотеке логики отображения для того, чтобы убедиться, что никакие классы не обращаются напрямую к Application Context. Вы не позволяете библиотеке ссылаться на какую-либо сборку ASP.NET (например, System.Web).

Рисунок 7-16 демонстрирует частичное представление архитектуры приложения. Основной момент – это тот факт, что вы используете классы центральной части приложения (Default Page и CampaignDataSource) в качестве Composition Root'ов, которые преобразуют классы уровня "Логика представления" совместно с их зависимостями.

Рисунок 7-16: Центральная часть приложения CampaignManagement – это единственная составляющая приложения, ссылающаяся на ASP.NET. Класс CampaignDataSource имеет конструктор по умолчанию, но действует как Composition Root или humble-объект, который делегирует все вызовы метода CampaignPresenter. Обычно стрелки обозначают указатели, а центральная часть приложения ссылается на все остальные модули приложения, поскольку она соединяет их вместе. Как модуль логики представления, так и модуль доступа к данным ссылаются на библиотеку доменной модели. Не все рассматриваемые классы продемонстрированы на этом рисунке.

Вооруженные знанием диаграммы зависимостей приложения, мы теперь можем реализовать Composition Root для кадра, продемонстрированного на рисунке 7-15.

Компоновка ObjectDataSource

Default Page, продемонстрированный на рисунке 7-15, состоит из элемента управления GridView и связанного с ним элемента управления ObjectDataSource. Как и в случае с классами Page, класс, используемый для ObjectDataSource также должен обладать конструктором по умолчанию. Для достижения этой цели вы специально создаете класс, продемонстрированный в следующем листинге.

Листинг 7-12: Компоновка Presenter в качестве источника данных
public class CampaignDataSource
{
	private readonly CampaignPresenter presenter;
	public CampaignDataSource()
	{
		var container =
			(CampaignContainer)HttpContext.Current
			.Application["container"];
		this.presenter = container.ResolvePresenter();
	}
	public IEnumerable<CampaignItemPresenter> SelectAll()
	{
		return this.presenter.SelectAll();
	}
	public void Update(CampaignItemPresenter item)
	{
		this.presenter.Update(item);
	}
}

Строка 9: Формирует Presenter

Строка 13, 17: Делегирует полномочия Presenter

Класс CampaignDataSource имеет конструктор по умолчанию, поскольку того требует ASP.NET. Близкий по духу принципу Fail Fast (принцип быстрого отказа), он незамедлительно пытается извлечь контейнер из Application Context и преобразовать экземпляр CampaignPresenter, который будет выступать в роли реальной реализации.

Все члены класса CampaignDataSource делегируют вызов преобразованному предъявителю, таким образом, действуя как humble-объект.

Примечание

Для приверженцев паттернов проектирования класс CampaignDataSource очень похож либо на Decorator, либо на Adapter. Он не реализует строго типизированный интерфейс, но обертывает соответствующую реализацию в класс, который соответствует требованиям, предъявляемых ASP.NET.

Вам может быть интересно, что мы приобретаем благодаря этому дополнительному уровню преобразования. Если вы привыкли к разработке через тестирование, то это должно быть вам понятно: HttpContext.Current недоступен во время модульного тестирования, поэтому вы не можете выполнить модульное тестирование CampaignDataSource. Это важная причина того, почему вы должны сохранять его humble-объектом.

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

Компоновка Presenter

Я не буду знакомить вас с подробной информацией о CampaignPresenter, но стоит рассмотреть сигнатуру его конструктора, поскольку он использует Constructor Injection:

public CampaignPresenter(CampaignRepository repository,
	IPresentationMapper mapper)

Зависимостями CampaignPresenter являются абстрактный класс CampaignRepository и интерфейс IPresentationMapper. Как раз то, что делают эти абстракции, менее важно, чем то, как вы их компонуете. Это является задачей CampaignContainer из следующего листинга. Вы можете вспомнить, что вы конфигурировали его в global.asax и регистрировали в Application Context.

Листинг 7-13: Преобразование CampaignPresenter
public CampaignPresenter ResolvePresenter()
{
	string connectionString =
		ConfigurationManager.ConnectionStrings
		["CommerceObjectContext"].ConnectionString;
	CampaignRepository repository =
		new SqlCampaignRepository(connectionString);
	IPresentationMapper mapper =
		new PresentationMapper();
	return new CampaignPresenter(repository, mapper);
}

Строка 6-7: Создает репозиторий

Строка 8-9: Создает преобразователь

Строка 10: Формирует Presenter

Ответственность метода ResolvePresenter – скомпоновать экземпляр CampaignPresenter. Из конструктора вы знаете, что для него нужен CampaignRepository, поэтому вы преобразовываете его в экземпляр SqlCampaignRepository. Другой зависимостью является IPresentationMapper, и вы преобразуете ее в конкретный класс PresentationMapper.

Вооружившись всеми необходимыми зависимостями, вы можете впоследствии вернуть новый экземпляр CampaignPresenter.

Использование механизма внедрения зависимостей в рамках ASP.NET невозможно. Главным недостатком использования каждого Page и источника данных объекта в виде объединенного Composition Root и humble-объекта является то, что для этого необходимо дублирование большинства членов класса.

Обратили ли вы внимание на то, как каждый член CampaignDataSource делегирует свою реализацию схожему по названию методу CampaignPresenter? Вам придется повторять эту идиому кода на протяжении всего ASP.NET приложения. Для каждого обработчика события нажатия кнопки вам необходимо определить и поддерживать в работоспособном состоянии связанный метод класса Presenter и тому подобное.

Как мы уже обсуждали в главе 3, я приравниваю понятие Composition Root к такому понятию Бережливой разработки программного обеспечения (Lean Software Development), как последний ответственный момент. В рамках таких фреймворков, как ASP.NET MVC и WCF, мы можем отложить композицию приложения вплоть до точки входа в приложение, но в ASP.NET так не делается. Не важно, как упорно мы стараемся, мы можем отложить только принятие решений о композиции объектов, пока не столкнемся с требованием о необходимости наличия конструктора по умолчанию.

Потом это становится "самым возможным местом", в котором мы можем компоновать объекты. Несмотря на то, что мы считаем, что пришли к компромиссу, мы все еще следуем всеобщему духу Composition Root. Мы компонуем иерархии объектов настолько близко к верхним уровням приложения, насколько это возможно, и разрешаем корректные DI-паттерны как здесь, так и на более низких по иерархии уровнях.

ASP.NET все еще предоставляет нам небольшую роскошь: мы можем использовать один экземпляр контейнера в рамках Application Context. Некоторые фреймворки не допускают даже это.

или RSS канал: Что новенького на smarly.net