Внедрение зависимостей в .NET
Марк Симан
Построение 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. Некоторые фреймворки не допускают даже это.