Главная страница   /   2.1. Как не стоит поступать (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

2.1. Как не стоит поступать

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

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

Рисунок 2-2: Стандартная архитектура трехуровневого приложения. Это самая простая общепринятая разновидность архитектуры n-уровневого приложения, где приложение состоит из n-уровней, каждый из которых состоит из одного или более одного модуля. Некоторые разновидности n-уровневых схем будут иметь вертикальные блоки, которые обозначают уровни составного приложения. Такие схемы часто используются для представления таких сквозных сущностей, как обеспечение безопасности и вход в систему.

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

Создание сильно связанного приложения

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

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

Знакомство с Мэри Роуэн

Мэри Роуэн – профессиональный .NET разработчик, работающая на местного сертифицированного партнера компании Microsoft, который, в основном, занимается разработкой веб-приложений. Ей 34 и с программным обеспечением она работает уже 11 лет. Это делает ее одним из самых опытных разработчиков компании, и она часто выступает в роли наставника для младших разработчиков в дополнение к выполнению ее регулярных обязанностей в качестве старшего разработчика.

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

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

Мэри попросили создать новое приложение электронной коммерции на платформе ASP.NET MVC и Entity Framework с SQL Server, используемым в качестве хранилища данных. Для того чтобы максимально увеличить модульность, это приложение должно быть трехуровневым.

Первой реализованной возможностью должен быть простой список рекомендуемых товаров, который вытаскивается из таблицы базы данных и отображается на веб-странице; пример продемонстрирован на рисунке 2-3. Если список просматривает привилегированный пользователь, то цена всех товаров должна быть снижена на 5 процентов.

Рисунок 2-3: Скриншот веб-приложения электронной коммерции, которое попросили создать Мэри. На нем изображен простой список рекомендуемых товаров и их цен ("kr." – это буквенный символ валюты для датских крон)

Давайте подсмотрим, как Мэри реализует первую возможность приложения.

Уровень данных

Поскольку Мэри нужно вытаскивать данные из таблицы базы данных, она решила начать с реализации уровня данных. Первый шаг – определить саму таблицу базы данных. Для создания таблицы, продемонстрированной на рисунке 2-4, Мэри использует SQL Server Management Studio.

Рисунок 2-4: Мэри создает таблицу Product при помощи SQL Server Management Studio; альтернативные подходы включают написание T-SQL скрипта, или создание таблицы с помощью Visual Studio или с помощью каких-либо других средств

Для реализации уровня "Data Access Layer" (уровень доступа к данным) Мэри добавляет в свое решение новую библиотеку. В Visual Studio она использует мастер Entity Data Model Wizard для того, чтобы сгенерировать целостную модель сущностей из базы данных, которую она ранее создала. Чтобы завершить оформление модели, она изменяет несколько названий, как это показано на рисунке 2-5.

Рисунок 2-5: Сущность Product, которая сгенерирована из таблицы базы данных Product, продемонстрированной на рисунке 2-4. Мэри изменила название столбца Featured на IsFeatured, а также изменила несколько названий в сгенерированном ObjectContext (не продемонстрировано).

Примечание

Не стоит волноваться, если вы не знакомы с Microsoft Entity Framework. Подробная информация о реализации доступа к данным в данном контексте не является важной, поэтому вам нужно всего лишь суметь последовать данному примеру, даже если вы более знакомы с другой технологией доступа к данным.

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

Рисунок 2-6 демонстрирует, как далеко Мэри зашла в реализации многоуровневой архитектуры, которая была представлена на рисунке 2-2.

Рисунок 2-6: До настоящего времени Мэри реализовывала уровень "Data Access Layer" (уровень доступа к данным) своего приложения. Уровни "Domain Logic Layer" (уровень доменной логики) и "User Interface Layer" (уровень пользовательского интерфейса) отодвинуты на второй план до тех пор, пока не будет реализована первая возможность.

Теперь, когда уровень доступа к данным реализован, следующий логичный шаг – реализация уровня доменной логики.

Доменный уровень

При отсутствии какой-либо доменной логики список товаров, предоставляемых с помощью сгенерированного ObjectContext, формально мог использоваться напрямую на уровне пользовательского интерфейса.

Предупреждение

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

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

Мэри: Мне нужно реализовать эту бизнес-логику так, чтобы привилегированный пользователь получал пятипроцентную скидку.

Дженс: Звучит легко. Просто умножь на 0,95.

Мэри: Спасибо, но я не об этом хотела тебя спросить. Я хотела спросить, как мне следует идентифицировать привилегированного пользователя?

Дженс: Понятно. У тебя веб-приложение или настольное приложение?

Мэри: Веб-приложение.

Дженс: Ясно, тогда ты можешь определить профиль пользователя и использовать свойство IsPreferredCustomer. Ты можешь получить профиль через HttpContext.

Мэри: Притормози-ка, Дженс. Этот код должен быть на уровне доменной логики. Это библиотека. Нет никакого HttpContext.

Дженс: О… (некоторое время думает). Я все еще считаю, что тебе нужно использовать возможность Profile ASP.NET для поиска значения по пользователю. Ты можешь потом передать значение в твою доменную логику как булево.

Мэри: Я не знаю…

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

Мэри: Думаю, ты прав.

Предупреждение

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

Вооружившись советом Дженса, Мэри создает новый проект библиотеки C# и добавляет класс с названием ProductService, который продемонстрирован в листинге ниже. Для того чтобы сделать класс ProductService компилируемым, она должна добавить ссылку на свою библиотеку Data Access, поскольку там определен класс CommerceObjectContext.

Листинг 2-1: Класс ProductService, добавленный Мэри
public partial class ProductService
{
	private readonly CommerceObjectContext objectContext;

	public ProductService()
	{
		this.objectContext = new CommerceObjectContext();
	}

	public IEnumerable<Product> GetFeaturedProducts(bool isCustomerPreferred)
	{
		var discount = isCustomerPreferred ? .95m : 1;
		var products = (from p in this.objectContext.Products
										where p.IsFeatured
										select p).AsEnumerable();
		return from p in products
						select new Product
						{
							ProductId = p.ProductId,
							Name = p.Name,
							Description = p.Description,
							IsFeatured = p.IsFeatured,
							UnitPrice = p.UnitPrice * discount
						};
	}
}

Мэри счастлива, что смогла инкапсулировать технологию доступа к данным (LINQ to Entities), конфигурацию и доменную логику в классе ProductService. Она делегировала знания о пользователе вызывающему оператору путем передачи параметра в isCustomerPreferred, и использует это значение для расчета скидки для всех товаров.

Дальнейшая обработка могла бы включать замену жестко-закодированного значения скидки (0,95) на настраиваемое число, но на данный момент такой реализации достаточно. Мэри почти все сделала – остался только пользовательский интерфейс. Мэри решила, что это может подождать до следующего дня.

Рисунок 2-7 демонстрирует, как далеко Мэри зашла в реализации архитектуры, представленной на рисунке 2-2.

Рисунок 2-7: На данном этапе Мэри реализовала уровень доступа к данным и уровень доменной логики. Если сравнивать с рисунком 2-6, то здесь добавлен уровень доменной логики. Уровень пользовательского интерфейса все еще остается не реализованным

После реализации уровня доступа к данным и уровня доменной логики остается реализовать только уровень пользовательского интерфейса.

Уровень пользовательского интерфейса

На следующий день Мэри возобновляет свою работу над приложением электронной коммерции, добавляя новое ASP.NET MVC приложение в свое решение.

Примечание

Не стоит волноваться, если вы не знакомы с фреймворком ASP.NET MVC. Запутанные детали того, как функционирует MVC framework, не рассматриваются в этой книге. Важной составляющей является то, как используются зависимости, а это относительно нейтральная тема.

Ускоренный курс по ASP.NET MVC

ASP.NET MVC берет свое название от паттерна проектирования Model View Controller. В данном контексте самое важное, что необходимо понять, – это то, что, когда поступает веб-запрос, контроллер управляет этим запросом, потенциально используя модель (доменную) для того, чтобы затронуть этот запрос и сформировать ответ, который, в конце концов, отображается с помощью представления.

Контроллер – это обычно класс, который наследуется от абстрактного класса Controller. У него есть один или более одного метода действия, которые управляют запросами, например. Класс HomeController обладает методом Index, который управляет запросом страницы, отображаемой по умолчанию.

При возврате метода действия он передает итоговую модель в представление посредством экземпляра ViewResult.

Следующий листинг демонстрирует, как Мэри реализует метод Index класса HomeController для того, чтобы извлечь обработанные товары из базы данных и передать их в представление. Для того чтобы сделать этот код компилируемым, ей нужно добавить ссылки как на библиотеку Data Access, так и на библиотеку Domain, поскольку класс ProductService определен в библиотеке Domain, а класс Product определен в библиотеке Data Access.

Листинг 2-2: Метод Index в используемом по умолчанию классе контроллера
public ViewResult Index()
{
	bool isPreferredCustomer = this.User.IsInRole("PreferredCustomer");

	var service = new ProductService();
	var products = service.GetFeaturedProducts(isPreferredCustomer);
	this.ViewData["Products"] = products;

	return this.View();
}

Являясь частью жизненного цикла ASP.NET MVC, свойство User класса HomeController автоматически заполняется корректным объектом пользователя, поэтому Мэри использует его для того, чтобы определить, является ли текущий пользователь привилегированным. Вооружившись этой информацией, она может вызвать доменную логику, чтобы получить список рекомендуемых товаров. Через некоторое время я вернусь к этому моменту, потому что здесь есть ловушка, но сейчас я позволю Мэри обнаружить эту ошибку самостоятельно.

Список товаров должен отображаться с помощью представления Index. Следующий листинг демонстрирует разметку для этого представления.

Листинг 2-3: Разметка представления Index
<h2>Featured Products</h2>
<div>
	<% var products = (IEnumerable<Product>)this.ViewData["Products"];
	foreach (var product in products)
	{ %>
	<div>
		<%= this.Html.Encode(product.Name) %>
		(<%= this.Html.Encode(product.UnitPrice.ToString("C")) %>)
	</div>
<% } %>
</div>

Строка 3: Получает товары, заполненные при помощи контроллера

ASP.NET MVC позволяет вам писать стандартный HTML-код с фрагментами императивного кода, который вставляется для получения доступа к объектам, созданным и переданным контроллером, который создал представление. В данном случае метод Index класса HomeController передал список рекомендуемых товаров ключу с названием Products, который Мэри использует в представлении для того, чтобы отображать список товаров.

Рисунок 2-8 демонстрирует, как Мэри реализовала архитектуры, показанную на рисунке 2-2.

Рисунок 2-8: На данный момент Мэри реализовала все три уровня приложения. Этот рисунок идентичен рисунку 2-2, но повторяется здесь для того, чтобы проиллюстрировать текущее состояние приложения Мэри.

Когда все три уровня находятся на своих местах, теоретически приложения должны работать, но только тест может проверить, действительно ли они работают.

Дымовой тест (Smoke test)

На данный момент Мэри реализовала все три уровня, поэтому пришло время понять, работает ли приложение. Она нажимает клавишу F5 и вскоре получает сообщение:

Указанное именованное соединение либо не найдено в конфигурации, либо не предназначено для использования в рамках EntityClient провайдера, либо не валидно.

Поскольку Мэри использовала конструктор по умолчанию CommerceObjectContext (продемонстрированный в листинге 2-1), то по смыслу ожидается, что строка соединения под названием CommerceObjectContext присутствует в файле web.config. Как я и упоминал в обсуждении листинга 2-2, эта имплицитность содержит в себе ловушку. За ночь Мэри забыла детали реализации ее доменного уровня. Код компилируется, но сайт не работает.

В этом случае фиксирование ошибки является прямолинейным. Мэри вставляет корректную строку соединения в файл web.config. При запуске приложения появляется продемонстрированная на рисунке 2-3 страница.

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

Анализ

Удалось ли Мэри создать совершенное, многоуровневое приложение? Нет, не удалось – несмотря на то, что у нее наверняка были самые наилучшие намерения. Она создала три Visual Studio проекта, которые соответствуют трем уровням запланированной архитектуры, что продемонстрировано на рисунке 2-9. Для обычного наблюдателя такая реализация похожа на желаемую архитектуру, но как вы увидите, такой код является сильно связанным.

Рисунок 2-9: В вэб-приложении электронной коммерции, созданном Мэри, присутствует по одному Visual Studio проекту для каждого уровня запланированной архитектуры – но является ли оно трехуровневым?

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

Диаграмма зависимостей

При работе с решениями в Visual Studio очень легко потерять путь перемещения важных зависимостей, поскольку Visual Studio показывает их вместе со всеми остальными ссылками проекта, которые могут указывать на сборки стандартной библиотеки классов .NET (BCL).

Для того чтобы понять, как модули приложения Мэри соотносятся друг с другом, мы можем нарисовать диаграмму зависимостей (см. рисунок 2-10).

Рисунок 2-10: Диаграмма зависимостей приложения Мэри, показывающая, каким образом модули зависят друг от друга. Стрелки указывают на зависимость модуля

Самым примечательным знанием, полученным из рисунка 2-10, является то, что библиотека пользовательского интерфейса (User Interface library) зависит как от доменной библиотеки (Domain library), так и от библиотеки доступа к данным (Data Access library). Кажется, что пользовательский интерфейс может обходиться в некоторых случаях без доменного уровня. Это предположение несет за собой дальнейшие исследования.

Анализ композиции

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

Для того чтобы оценить реализацию Мэри, мы можем задать простой вопрос:

Тест

Возможно ли использовать каждый модуль в изоляции от других модулей?

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

Примечание

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

Можем ли мы использовать модули приложения Мэри новыми и увлекательными способами? Давайте рассмотрим некоторые возможные сценарии.

Новый пользовательский интерфейс

Если приложение Мэри будет иметь успех, то заинтересованные стороны проекта захотят, чтобы она разработала полную клиентскую версию в системе построения клиентских приложений Windows – Windows Presentation Foundation (WPF). Возможно ли это сделать во время повторного использования доменного уровня и уровня доступа к данным?

При рассмотрении диаграммы зависимостей на рисунке 2-10 мы можем быстро определить, что ни один из модулей не зависит от пользовательского веб-интерфейса, поэтому можно удалить его и заменить на WPF пользовательский интерфейс.

Создание полной клиентской версии на базе WPF – это новое приложение, которое использует большинство своих реализаций совместно с первоначальным веб-приложением. Рисунок 2-11 иллюстрирует, как WPF приложению может понадобиться использовать те же самые зависимости, что и веб-приложение. Первоначальное веб-приложение может оставаться неизмененным.

Рисунок 2-11: Замена пользовательского веб-интерфейса пользовательским WPF интерфейсом возможна, так как ни один из модулей не зависит от пользовательского веб-интерфейса. Первоначальный пользовательский веб-интерфейс на рисунке остается выделенным серым цветом для того, чтобы проиллюстрировать, что добавление нового пользовательского интерфейса не исключает первоначальный пользовательский интерфейс

Замена уровня пользовательского интерфейса наверняка возможна в реализации Мэри, поэтому давайте рассмотрим еще одну интересную декомпозицию.

Новый уровень доступа к данным

Представьте себе, что рыночные аналитики поймут, что для оптимизации прибыли приложение Мэри должно быть доступным в виде облачного приложения, размещенного на Windows Azure. В Windows Azure данные могут храниться в весьма масштабируемом сервисе Azure Table Storage Service. Этот механизм хранения данных основан на гибких data-контейнерах, которые содержат не свободные данные. Сервис не навязывает никакой конкретной схемы базы данных, и отсутствует соответствующая целостность.

Протокол, используемый для взаимодействия с Table Storage Service, – это HTTP, а наиболее очевидная .NET технология доступа к данным основывается на ADO.NET Data Services.

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

Чтобы дать возможность использовать приложение электронной коммерции в виде облачного приложения, библиотеку доступа к данным можно заменить модулем, который использует Table Storage Service. Возможно ли это?

Из диаграммы зависимостей рисунка 2-10 мы уже знаем, что и библиотека пользовательского интерфейса, и доменная библиотека зависят от библиотеки доступа к данным, которая базируется на Entity Framework. Если мы попытаемся удалить библиотеку доступа к данным, то решение больше не будет компилироваться, поскольку отсутствует необходимая зависимость.

В большом приложении, состоящем из дюжины модулей, мы также могли бы попытаться удалить те модули, которые не компилируются, чтобы увидеть, что останется. Что касается приложения Мэри, то здесь очевидно, что нам пришлось бы удалить все модули, ничего при этом не оставляя.

Несмотря на то, что можно было бы разработать библиотеку Azure Table Data Access, которая имитирует API, используемое первоначальной библиотекой доступа к данным, нет ни одного способа, с помощью которого мы бы могли внедрить ее в приложение.

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

Рисунок 2-12: Попытка удаления реляционной библиотеки доступа к данным приводит к тому, что ничего не остается, потому что все остальные модули зависят от нее. Нет ни одного места, где мы могли дать доменной библиотеке указание использовать новую библиотеку Azure Table Data Access вместо первоначально используемой

Другие комбинации

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

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

Анализ

Почему реализация Мэри не достигла желаемой степени компоновки? Не произошло ли это, потому что пользовательский интерфейс напрямую зависит от библиотеки доступа к данным? Давайте исследуем эту возможность более подробно.

Анализ диаграммы зависимостей

Почему пользовательский интерфейс зависит от библиотеки доступа к данным? Виновником этого является сигнатура метода данной доменной модели:

public IEnumerable<Product> GetFeaturedProducts(bool isCustomerPreferred)

Product: Раскрывает тип доступа к данным для клиентов

Метод GetFeaturedProducts возвращает последовательность товаров, но класс Product определен в библиотеке доступа к данным. Любой клиент, использующий метод GetFeaturedProducts должен ссылаться на библиотеку доступа к данным для возможности компиляции.

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

Давайте предположим, что мы разрушаем зависимость между библиотекой пользовательского интерфейса и библиотекой доступа к данным. Измененная диаграмма зависимостей выглядела бы сейчас так, как показано на рисунке 2-13.

Рисунок 2-13: Диаграмма зависимостей гипотетической ситуации, при которой разрывается зависимость пользовательского интерфейса от библиотеки доступа к данным

Позволило бы Мэри такое изменение заменить реляционную библиотеку доступа к данным такой библиотекой, которая инкапсулирует доступ к сервису Azure Table? К несчастью, не позволило бы, поскольку доменная библиотека все еще зависит от библиотеки доступа к данным. Пользовательский интерфейс, в свою очередь, все еще зависит от доменной модели, поэтому если мы попытаемся удалить первоначальную библиотеку доступа к данным, то в приложении ничего не останется.

Основная причина этой проблемы находится где-то в другом месте.

Анализ интерфейса доступа к данным

Доменная модель зависит от библиотеки доступа к данным, поскольку вся модель данных определена в этой библиотеке. Класс Product был сгенерирован, когда Мэри запускала мастер LINQ to Entities. Использование Entity Framework для реализации уровня доступа к данным может стать разумным решением.

Как бы то ни было, не используйте его напрямую в доменной модели.

Код, являющийся виновником возникшей проблемы, разбросан по классу ProductService. Конструктор создает новый экземпляр класса CommerceObjectContext и передает его в приватную переменную члена класса:

this.objectContext = new CommerceObjectContext();

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

Реализация метода GetFeaturedProducts использует CommerceObjectContext для того, чтобы вытянуть объекты Product из базы данных.

var products = (from p in this.objectContext.Products
								where p.IsFeatured
								select p).AsEnumerable();

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

Прочие вопросы

Перед тем как показать вам лучшую альтернативу, мне хотелось бы обратить ваше внимание на некоторые другие вопросы, связанные с кодом Мэри, к которым обязательно нужно обратиться.

  • Кажется, что большинство доменных моделей реализуются в библиотеке доступа к данным. Несмотря на то, что тот факт, что библиотека доменной модели ссылается на библиотеку доступа к данным, является технической проблемой, то, что библиотека доступа к данным определяет такой класс, как класс Product, что является, в свою очередь, концептуальной проблемой. Открытый класс Product принадлежит к доменной модели.
  • Оказавшись под влиянием Дженса, Мэри решила реализовать код, который определяет, является ли пользователь привилегированным покупателем в пользовательском интерфейсе. Тем не менее, то, каким образом покупатель идентифицируется в качестве привилегированного, является фрагментом бизнес-логики, поэтому это должно быть реализовано в доменной модели.

    Аргумент Дженса в пользу концепции разделения и принципа единственной ответственности никак не оправдывает размещение кода в неправильном месте. Следование принципу единственной ответственности в рамках единичной библиотеки вполне возможно – это ожидаемый подход.
  • Класс ProductService полагается на XML конфигурацию. Как вы видели при наблюдении за стараниями Мэри, она забыла, что ей пришлось поместить часть конфигурационного кода в ее файл web.config. Несмотря на то, что способность конфигурирования компилируемого приложения является важной, только окончательное приложение должно полагаться на конфигурационные файлы. Удобнее, когда повторно используемые библиотеки обязательно конфигурируются вызывающими их объектами.

    В конце концов, конечный вызывающий объект сам по себе является приложением. На данном этапе все соответствующие конфигурационные данные можно прочитать из .config файла и загрузить при необходимости в основные библиотеки.
  • Кажется, что представление (продемонстрированное в листинге 2-3) содержит слишком много функциональности. Оно выполняет расчеты и конкретное строковое форматирование. Такая функциональность должна быть перемещена в основную модель.

В следующем разделе я продемонстрирую вам более скомпонованный способ создания приложения с теми же самыми возможностями, которые создавала Мэри. Кроме того, я также обращусь к этим второстепенным вопросам.