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

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

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

Марк Симан

2.2. Как действовать правильно

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

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

Примечание

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

Многие люди называют механизм внедрения зависимостей инверсией управления (Inversion of Control). Иногда эти термины используются как взаимозаменяемые, но DI – это подмножество инверсии управления. На протяжении всей этой книги я буду последовательно использовать самый конкретный термин: DI. Если я буду иметь ввиду инверсию управления, то я буду говорить об этом конкретно.

Внедрение зависимостей или Инверсия управления?

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

При написании ASP.NET приложения вы попадаете в жизненный цикл ASP.NET страницы, но вы ничем не управляете, всем управляет ASP.NET.

При написании WCF сервиса вы реализуете интерфейсы, помеченные атрибутами. Вы можете писать код сервиса, но, в конце концов, вы ничем не управляете, всем управляет WCF.

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

До того момента, как механизм внедрения зависимостей приобрел свое название, люди начинали называть фреймворки, которые управляли зависимостями, IoC-контейнерами, и вскоре смысл IoC постепенно сместился к конкретному значению: инверсия управления зависимостями. Будучи постоянным систематиком, Мартин Фаулер ввел термин внедрение зависимостей для того, чтобы в частности ссылаться на IoC в контексте управления зависимостями. С тех пор внедрение зависимостей широко применяется в качестве самого корректного термина.

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

В контексте управления зависимостями инверсия управления в точности описывает то, что мы пытаемся выполнить. В приложении Мэри код напрямую управляет его зависимостями: когда ProductService требуется новый экземпляр класса CommerceObjectContext, он просто создает экземпляр с помощью ключевого слова new. Когда HomeController требуется новый экземпляр класса ProductService, он также создает новый экземпляр с помощью ключевого слова new. Приложение находится под тотальным контролем. Это может звучать сильно, но в действительности контроль ограничен. Я называю это анти-паттерном Control Freak (прим. ред. – термин в психологии характеризует человека, который пытается диктовать всем вокруг как, по его мнению, должно все происходить. В переводе, с точки зрения анти-паттерна, упоминается как Руководитель-наркоман, но мы оставим английский вариант). Инверсия управления дает нам указание отпустить этот контроль и позволить еще кому-то управлять зависимостями.

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

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

Примечание

Методика "снаружи-внутрь" тесно связана с принципом YAGNI ("You Aren’t Gonna Need It") – "Вам это не понадобится". Этот принцип подчеркивает, что необходимо реализовывать только требуемые возможности, и что реализация должна быть настолько простой, насколько это возможно.

Так как я всегда практикую разработку через тестирование (TDD), я начинаю писать модульные тесты, как только используемый мной подход "снаружи-внутрь" начинает намекать мне на создание нового класса. Несмотря на то, что я написал множество модульных тестов, чтобы создать этот пример, разработка через тестирование не требует реализации и использования механизма внедрения зависимостей, поэтому я не собираюсь демонстрировать эти тесты в моей книге. Если вам интересно, то эти тесты доступны в исходном коде, который приложен к этой книге.

Пользовательский интерфейс

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

Первое, что я делаю после открытия Visual Studio – добавляю в мое решение новое ASP.NET MVC приложение. Так как список рекомендуемых товаров должен продолжать титульную страницу, я начинаю с изменения Index.aspx таким образом, чтобы она содержала разметку, продемонстрированную в следующем листинге.

Листинг 2-4: Разметка Index представления
<h2>Featured Products</h2>
<div>
	<% foreach (var product in this.Model.Products)
	{ %>
		<div><%= this.Html.Encode(product.SummaryText) %></div>
	<% } %>
</div>

Заметьте, насколько чище листинг 2-4 по сравнению с листингом 2-3. Первое усовершенствование – это то, что больше не нужно приводить элемент словаря к последовательности товаров до того, как итерация станет возможной. Я с легкостью выполнил это, разрешив странице Index.aspx наследоваться от System.Web.Mvc.ViewPage<FeaturedProductsViewModel>, а не от System.Web.Mvc.ViewPage. Это означает, что свойство Model страницы имеет тип FeaturedProductsViewModel.

Вся строка отображения товара берется прямо из свойства SummaryText этого товара.

Оба усовершенствования связаны с введением моделей конкретных представлений, которые инкапсулируют поведение представления. Эти модели являются POCO-объектами (Plain Old CLR Objects). Рисунок 2-14 предоставляет обзор структуры таких объектов.

Рисунок 2-14: FeaturedProductsViewModel содержит список ProductViewModels. И FeaturedProductsViewModel, и ProductViewModel являются POCO-объектами, что делает их в высокой степени доступными для модульного тестирования. Свойство SummaryText полученно из свойств Name и UnitPrice для того, чтобы инкапсулировать логику отображения.

Чтобы код из листинга 2-4 был рабочим, HomeController должен возвращать представление с экземпляром FeaturedProductsViewModel. Например, на первом шаге он может быть реализован внутри HomeController, как это продемонстрировано ниже:

public ViewResult Index()
{
	var vm = new FeaturedProductsViewModel();
	return View(vm);
}

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

Рисунок 2-15 демонстрирует текущее состояние реализации архитектуры, показанной на рисунке 2-2.

Рисунок 2-15: На данном этапе реализован только уровень пользовательского интерфейса, осталось реализовать уровни доменной логики и доступа к данным. Сравним этот рисунок с рисунком 2-6, который демонстрирует успехи Мэри на этом же самом этапе. Одним из преимуществ того, что мы начинаем создание приложения с пользовательского интерфейса, является то, что мы уже получаем приложение, которое можно запускать и тестировать. Только на более позднем этапе, продемонстрированном на рисунке 2-8, Мэри приходит к тому моменту, когда она может запускать и тестировать свое приложение.

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

Доменная модель

Доменная модель – это обычная, несложная C# библиотека, которую я добавляю в свое решение. Эта библиотека будет содержать POCO-объекты и абстрактные типы. POCO-объекты будут моделировать домен, в то время, как абстрактные типы обеспечивают абстракции, которые будут выступать в роли моих основных внешних записей в доменной модели.

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

Интерфейсы или абстрактные классы?

Многие руководства по объектно-ориентированному проектированию фокусируются на интерфейсах как на главном механизме абстракций, в то время, как руководства по проектированию на базе .NET Framework поддерживают превосходство абстрактных классов над интерфейсами. Следует ли вам использовать интерфейсы или же все-таки абстрактные классы?

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

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

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

Универсальная абстракция доступа к данным обеспечивается паттерном Repository, поэтому я определю абстрактный класс ProductRepository в библиотеке доменной модели.

public abstract class ProductRepository
{
	public abstract IEnumerable<Product> GetFeaturedProducts();
}

Готовый Repository будет иметь больше методов для поиска и изменения товаров, но следуя принципу "снаружи-внутрь", я определяю только классы и члены, которые мне нужны для текущей задачи. Проще добавить функциональность в код, чем удалить все. Класс Product также реализован с самым минимальным количеством членов, что проиллюстрировано на рисунке 2-16.

Рисунок 2-16: Класс Product содержит только свойства Name и UnitPrice, поскольку это единственные свойства, которые нужны для реализации желаемой возможности приложения. ApplyDiscountFor применяет в случае необходимости предоставить скидку для пользователя и возвращает экземпляр класса DiscountedProduct. Абстрактный GetFeaturedProducts возвращает последовательность Products.

Метод Index HomeController должен использовать экземпляр ProductService для того, чтобы извлечь список рекомендуемых товаров, применить какую-либо скидку, сконвертировать экземпляры Product в экземпляры ProductViewModel, затем добавить их в FeaturedProductsViewModel. Поскольку класс ProductService принимает в свой конструктор экземпляр ProductRepository, самое сложное – обеспечить его соответствующим экземпляром. Вспомните из анализа реализации Мэри, что создание зависимостей при помощи ключевого слова new является неправильным. Как только я это сделаю, я окажусь сильно связанным с типом, который я только что использовал.

Я собираюсь отказаться от контроля над зависимостью ProductRepository. Как продемонстрировано в следующем листинге, я лучше буду полагаться на что-нибудь другое, чтобы получить экземпляр с помощью конструктора HomeController. Этот паттерн называется Constructor Injection (внедрение через конструктор) – как и кем создается экземпляр не является заботой HomeController.

Листинг 2-5: HomeController с паттерном Constructor Injection
public partial class HomeController : Controller
{
	private readonly ProductRepository repository;

	public HomeController(ProductRepository repository)
	{
		if (repository == null)
		{
			throw new ArgumentNullException("repository");
		}
		this.repository = repository;
	}

	public ViewResult Index()
	{
		var productService = new ProductService(this.repository);

		var vm = new FeaturedProductsViewModel();

		var products = productService.GetFeaturedProducts(this.User);
		foreach (var product in products)
		{
			var productVM = new ProductViewModel(product);
			vm.Products.Add(productVM);
		}
		return View(vm);
	}
}

Строка 5: Внедрение через конструктор

Строка 11: Сохраняет внедренную зависимость для дальнейшего использования

Строка 16: Передает внедренную зависимость

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

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

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

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

Листинг 2-6: Класс ProductService
public class ProductService
{
	private readonly ProductRepository repository;

	public ProductService(ProductRepository repository)
	{
		if (repository == null)
		{
			throw new ArgumentNullException("repository");
		}
		this.repository = repository;
	}

	public IEnumerable<DiscountedProduct> GetFeaturedProducts(IPrincipal user)
	{
		if (user == null)
		{
			throw new ArgumentNullException("user");
		}

		return from p in this.repository.GetFeaturedProducts()
						select p.ApplyDiscountFor(user);
	}
}

Строка 5: И снова используем Constructor Injection

Строка 14: Паттерн Method Injection

Строка 21-22: Используем обе внедренные зависимости для реализации поведения

Метод GetFeaturedProducts теперь принимает в качестве параметра экземпляр IPrincipal, который представляет собой текущего пользователя. Это еще одно отклонение от реализации Мэри, показанной в листинге 2-1, которая всего лишь принимала в качестве параметра булево значение, указывая, является ли пользователь привилегированным. Тем не менее, поскольку решение о том, является ли пользователем привилегированным, – это часть доменной логики, корректнее будет явно смоделировать текущего пользователя в виде зависимости. Мы должны всегда соблюдать принцип программирования на основании интерфейса, но в данном случае мне не нужно ничего изобретать (как я это делал с ProductRepository), потому что в библиотеку стандартных классов .NET уже входит интерфейс IPrincipal, который представляет собой стандартный способ моделирования пользователей приложения.

Передача зависимости в метод в качества параметра называется внедрением через параметры метода (Method Injection). И снова управление делегируется вызывающему оператору, подобно внедрению через конструктор. Несмотря на то, что детали могут варьироваться, основная технология остается той же.

На данном этапе приложение все еще не работает. Осталось две проблемы:

  • Отсутствует конкретная реализация ProductRepository. Эта проблема легко решается. В следующем разделе я буду реализовывать конкретный ProductRepository, который считывает рекомендуемые товары из базы данных.
  • По умолчанию ASP.NET MVC предполагает, что контроллеры обладают конструкторами по умолчанию. Поскольку я ввел параметр в конструктор HomeController, MVC framework не знает, как создать экземпляр HomeController. Эта проблема может быть решена путем создания пользовательского IControllerFactory. То, как это делается, выходит за рамки данной главы, но этот вопрос будет обсуждаться в главе 8. Достаточно просто сказать, что эта пользовательская фабрика создаст экземпляр конкретного ProductRepository и передаст его в конструктор HomeController.

В доменной модели я работаю только с типами, определенными в рамках доменной модели (и библиотеки стандартных классов. NET). Сущности доменной модели реализуются в виде POCO-объектов. На данном этапе существует одна единственная представленная сущность, названная Product. Доменная модель должна уметь взаимодействовать с внешним миром (например, с базами данных). Эта необходимость смоделирована в виде абстрактных классов (например, Repositories), которые мы должны заменить конкретными реализациями перед тем, как доменная модель станет полезной.

Рисунок 2-17 демонстрирует текущее состояние реализации архитектуры, продемонстрированной на рисунке 2-2.

Рисунок 2-17: Уровень пользовательского интерфейса и уровень доменной логики уже реализованы, остается реализовать уровень доступа к данным. Сравните этот рисунок с рисунком 2-7, который демонстрирует успехи Мэри на данном этапе.

Доменная модель приложения еще не является объектно-ориентированной; чтобы завершить цикл, мне нужно реализовать только один абстрактный ProductRepository.

Доступ к данным

Как и Мэри, мне хотелось бы реализовать мою библиотеку доступа к данным при помощи технологии LINQ to Entities, поэтому я последую тем же шагам, которые она выполняла в разделе "Создание сильно связанного приложения" при создании модели. Главное отличие – модель и CommerceObjectContext теперь являются всего лишь деталями реализации; но с помощью них я могу создать реализацию ProductRepository, что продемонстрировано в следующем листинге.

Листинг 2-7: Реализация ProductRepository с помощью LINQ to Entities
public class SqlProductRepository : Domain.ProductRepository
{
	private readonly CommerceObjectContext context;

	public SqlProductRepository(string connString)
	{
		this.context = new CommerceObjectContext(connString);
	}

	public override IEnumerable<Domain.Product> GetFeaturedProducts()
	{
		var products = (from p in this.context.Products
										where p.IsFeatured
										select p).AsEnumerable();
		return from p in products
						select p.ToDomainProduct();
	}
}

Строка 16: Конвертирует в Domain Product

В приложении Мэри сгенерированная сущность Product использовалась в качестве доменного объекта, несмотря на то, что она была определена в базе данных. Так больше не происходит, потому что я уже определил класс Product в доменной модели. Когда я сгенерировал модуль, мастер создал для меня еще один класс Product и мне нужно выполнить конвертацию между этими классами. Рисунок 2-18 иллюстрирует, каким образом они определены в разных модулях. Существующий класс Product – это всего лишь деталь реализации, и я мог бы с легкостью сделать его внутренним, чтобы выразить его более явно.

Рисунок 2-18: И библиотека доменной модели, и библиотека доступа к данным определяют класс под названием Product. Доменный класс Product – это важный класс, который инкапсулирует доменную сущность – товар. Класс Product библиотеки доступа к данным – это всего лишь искусственный объект мастера Entity Framework. Его можно легко переименовать или сделать внутренним.

Примечание

Вы можете поспорить, что то, что Entity Framework не поддерживает неграмотные сущности, является его определенным дефектом (по крайней мере, не в версии .NET 3.5 SP1). Тем не менее, это вид ограничения, с которым вы обязательно столкнетесь в реальных проектах программного обеспечения.

Существующий Product задает конвертацию в доменный тип Product. Эта конвертация является обычным преобразованием соответствующих значений. Несмотря на то, что такая конвертация не относится конкретно к механизму внедрения зависимостей, я использую ее для более удобных измерений:

Domain.Product p = new Domain.Product();
p.Name = this.Name;
p.UnitPrice = this.UnitPrice;
return p;

После реализации SqlProductRepository я теперь могу настроить ASP.NET MVC для того, чтобы внедрить экземпляр SqlProductRepository в экземпляры HomeController. Поскольку я более подробно буду рассматривать это в главе 7, я не буду демонстрировать это здесь.

Рисунок 2-19 демонстрирует текущее состояние архитектуры приложения, продемонстрированной на рисунке 2-2.

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

Теперь, когда все корректно соединено вместе, я могу перейти к домашней странице приложения и получить такую же страницу, как продемонстрированная на рисунке 2-3.

Анализ слабо связанной реализации

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

Взаимодействие

Классы каждого уровня взаимодействуют друг с другом напрямую или в абстрактной форме. Таким образом, они работают за пределами модулей, поэтому трудно будет проследить процесс их взаимодействия. Рисунок 2-20 иллюстрирует, как соединяются зависимости.

Рисунок 2-20: Взаимодействие между элементами, включенными в механизм внедрения зависимостей в коммерческом приложении. Заметьте, что экземпляр SqlProductRepository внедряется в HomeController, а затем спустя некоторое время через HomeController внедряется в ProductService, который, в конечном счете, использует его.

При запуске приложения код файла Global.asax создает новую пользовательскую фабрику контроллеров. Приложение сохраняет ссылку на фабрику контроллеров, поэтому, когда выполняется запрос страницы, приложение вызывает CreateController для фабрики. Фабрика ищет строку соединения в файле web.config и передает ее в новый экземпляр SqlProductRepository. Фабрика внедряет экземпляр SqlProductRepository в новый экземпляр HomeController, а затем возвращает этот экземпляр.

Затем приложение вызывает метод Index для экземпляра HomeController, заставляя его создать новый экземпляр ProductService, передавая экземпляр SqlProductRepository в его конструктор. ProductService вызвает метод GetFeaturedProducts для экземпляра SqlProductRepository.

В конце концов, возвращается ViewResult с заполненным FeaturedProductsViewModel, а ASP.NET MVC фреймворк находит и отображает корректную страницу.

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

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

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

Рисунок 2-21: Диаграмма зависимостей, которая демонстрирует пример коммерческого приложения при использовании механизма внедрения зависимостей. Самое примечательное отличие – это то, что у доменной библиотеки больше нет зависимостей. Серые блоки внутри черных блоков демонстрируют примерные классы каждой библиотеки для того, чтобы дать вам представление о том, где какие классы должны быть. В каждой библиотеке присутствует больше классов, чем продемонстрировано на этом рисунке.

Это пробуждает наши надежды на то, что мы более плодотворно сможем ответить на первоначальные вопросы о композиции:

  • Можем ли мы заменить пользовательский веб-интерфейс пользовательским интерфейсом WPF? Такая замена была возможна до этого и остается возможной в рамках нового проектирования. Ни библиотека доменной модели, ни библиотека доступа к данным не зависят от пользовательского веб-интерфейса, поэтому мы можем с легкостью поместить на его место что-то другое.
  • Можем ли мы заменить реляционную библиотеку доступа к данным библиотекой, которая работает с сервисом Azure Table Service? В главе 3 я буду описывать, как приложение размещает и создает экземпляры корректного ProductRepository, поэтому на данный момент примите следующее как данность: библиотека доступа к данным загружается с помощью позднего связывания, а имя типа определяется в виде настройки приложения в файле web.config. Можно удалить текущую библиотеку доступа к данным и внедрить новую библиотеку, поскольку она также предоставляет реализацию ProductRepository.

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

Благодаря проектированию на основании механизма внедрения зависимостей первоначальное веб-приложение может постепенно быть преобразовано в приложение Software + Services (доступ к программному обеспечению предоставляется заказчику через интернет) с богатым WPF интерфейсом и облачным движком хранилища данных. Единственное, что осталось от первоначальной работы – это доменная модель, но это целесообразно, поскольку доменная модель инкапсулирует все важные бизнес-правила, и по существу, нам следует ожидать, что она будет самым существенным модулем.

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