Главная страница   /   18.1. Знакомство с механизмом внедрения зависимостей (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

18.1. Знакомство с механизмом внедрения зависимостей

Перед рассмотрением того, как можно эффективно использовать внедрение зависимостей (DI) в наших ASP.NET MVC приложениях, важно понять происхождение DI, чтобы вы поняли, почему этот механизм полезен.

Несмотря на то, что эта тема довольно широка, и ей посвящены целые книги (к примеру, книга Марка Симанна "Dependency Injection in .NET", http://manning.com/seemann/), мы вкратце ознакомим вас с некоторыми основами. Мы начнем с того, что рассмотрим создание простой системы и изучим, как DI могут использоваться для улучшения ее дизайна. Вслед за этим мы рассмотрим то, как можно использовать DI-контейнер для того, чтобы упростить некоторое повторяющееся кодирование.

Что такое DI?

Для иллюстрации понятия DI мы рассмотрим конструирование простой системы, связанной с печатью документов. Такая система может выполнять несколько задач – она сначала должна извлечь документ, затем ей необходимо отформатировать документ так, чтобы он имел понятный принтеру формат, и, наконец, ей необходимо напечатать документ.

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

  • Класс Document мог бы представлять документ, который необходимо напечатать.
  • Класс DocumentRepository мог бы отвечать за извлечение документов из файловой системы.
  • Класс DocumentFormatter мог бы принимать экземпляр Document и форматировать его для печати.
  • Класс Printer мог бы взаимодействовать с физическим принтером.
  • Суммарный класс DocumentPrinter мог бы отвечать за распределение других компонентов.

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

Листинг 18-1: Взаимодействие компонентов, предназначенных для печати документа
public class DocumentPrinter
{
	public void PrintDocument(string documentName)
	{
		var repository = new DocumentRepository();
		var formatter = new DocumentFormatter();
		var printer = new Printer();
		var document = repository
			.GetDocumentByName(documentName);
		var formattedDocument = formatter.Format(document);
		printer.Print(formattedDocument);
	}
}

Строки 5-7: Создание экземпляров компонентов

Строки 8-9: Извлечение документа по имени

Строка 10: Форматирование документа

Строка 11: Печать документа

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

Затем DocumentRepository используется для извлечения документа с конкретным названием. Этот документ затем передается в DocumentFormatter, который форматирует его для печати и возвращает отформатированный документ. Наконец, отформатированный документ отправляется на принтер.

Мы могли бы использовать класс DocumentPrinter в нашем коде путем создания экземпляра этого класса и вызова метода PrintDocument:

var documentPrinter = new DocumentPrinter();
documentPrinter.PrintDocument("C:/MVC3InAction/Manuscript.doc");

В данный момент наш DocumentPrinter не использует DI. Внутри создаются экземпляры всех его зависимостей, что означает, что он сильно связан с этими компонентами. Например, если бы мы ввели новый класс, который извлекал бы документы из базы данных, а не из файловой системы, то нам пришлось бы модифицировать DocumentPrinter так, чтобы он создавал экземпляр этого нового DatabaseDocumentRepository вместо использования первоначального DocumentRepository. DI позволяет нам разорвать эту связь.

Использование внедрения через конструктор

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

Листинг 18-2: DocumentPrinter, использующий внедрение через конструктор
public class DocumentPrinter
{
	private DocumentRepository _repository;
	private DocumentFormatter _formatter;
	private Printer _printer;
	public DocumentPrinter(
		DocumentRepository repository,
		DocumentFormatter formatter,
		Printer printer)
	{
		_repository = repository;
		_formatter = formatter;
		_printer = printer;
	}
	public void PrintDocument(string documentName)
	{
		var document = _repository.GetDocumentByName(documentName);
		var formattedDocument = _formatter.Format(document);
		_printer.Print(formattedDocument);
	}
}

Строки 3-5: Сохраняет зависимости в полях

Строки 6-14: Внедряет зависимости в конструктор

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

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

var repository = new DocumentRepository();
var formatter = new DocumentFormatter();
var printer = new Printer();
var documentPrinter = new DocumentPrinter(repository, formatter, printer);
documentPrinter.PrintDocument("C:/MVC3InAction/Manuscript.doc");

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

Знакомство с интерфейсами

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

public interface IDocumentRepository
{
	Document GetDocumentByName(string documentName);
}

Далее у нас может быть два класса, реализующих этот интерфейс – FilesystemDocumentRepository и DatabaseDocumentRepository. То же самое мы можем сделать и для других зависимостей DocumentPrinter.

Теперь можно выполнить рефакторинг DocumentPrinter для того, чтобы принять зависимость от интерфейса, а не от конкретного класса. Новую структуру приложения можно увидеть на рисунке 18-1, а реорганизованный код продемонстрирован в листинге 18-3.

Рисунок 18-1: DocumentPrinter зависит от интерфейсов, а не от конкретных реализаций
Листинг 18-3: DocumentPrinter, использующий внедрение через конструктор
public class DocumentPrinter
{
	private IDocumentRepository _repository;
	private IDocumentFormatter _formatter;
	private IPrinter _printer;
	public DocumentPrinter(
		IDocumentRepository repository,
		IDocumentFormatter formatter,
		IPrinter printer)
	{
		_repository = repository;
		_formatter = formatter;
		_printer = printer;
	}
	public void PrintDocument(string documentName)
	{
		var document = _repository.GetDocumentByName(documentName);
		var formattedDocument = _formatter.Format(document);
		_printer.Print(formattedDocument);
	}
}

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

Например, мы могли бы передать fake-реализацию IPrinter в конструктор, которая помогла бы нам выполнить модульное тестирование DocumentPrinter без реальной отправки страниц на реальный принтер каждый раз, когда мы запускаем тест! Более подробно о методиках использования тестовых fake-копий можно прочитать в книге Роя Ошерова "The Art of Unit Testing" (http://manning.com/osherove/).

Несмотря на то, что DocumentPrinter был отделен от своих зависимостей, наш код вызова на данный момент стал более сложным. Каждый раз, когда мы создаем экземпляр объекта, нам приходится запоминать, экземпляры каких реализаций зависимостей нам необходимо создать. Данный процесс можно автоматизировать посредством использования DI-контейнера.

Использование DI-контейнера

DI-контейнер, в сущности, является умной фабрикой. Как и любой другой класс фабрики, он отвечает за создание экземпляров объектов, но он также знает, как создавать экземпляры зависимостей объектов. Это означает, что мы можем попросить контейнер создать DocumentPrinter, и он также знает, как создать экземпляры всех зависимостей и передать их в конструктор.

Существует несколько DI-контейнеров, доступных в .NET. Некоторые самые популярные – это StructureMap, Castle Windsor, Ninject, Autofac и Unity. Все контейнеры служат одной и той же цели, но они различаются по конструкции API и дополнительной функциональности. Мы решили использовать в наших примерах StructureMap в связи с его мощным API и популярностью, но те же самые методики применимы и к другим контейнерам.

StructureMap можно загрузить с http://structuremap.sourceforge.net или установить посредством менеджера пакетов NuGet. Как только на него будет ссылаться наше приложение, вы сможете начать использовать класс ObjectFactory, который располагается в пространстве имен StructureMap.

Перед тем, как мы сможем использовать ObjectFactory, нам необходимо настроить его таким образом, чтобы он знал, как преобразовывать интерфейсы к конкретным типам:

ObjectFactory.Configure(cfg =>
{
	cfg.For<IDocumentRepository>().Use<FilesystemDocumentRepository>();
	cfg.For<IDocumentFormatter>().Use<DocumentFormatter>();
	cfg.For<IPrinter>().Use<Printer>();
});

Мы вызываем метод Configure для ObjectFactory, передавая анонимный метод, который позволяет нам получить доступ к настройке контейнера. Внутри анонимного метода мы можем использовать методы For и Use для того, чтобы рассказать StructureMap, как преобразовывать интерфейс в конкретный тип. Например, в данном примере мы говорим StructureMap, что когда бы он ни встречал IDocumentRepository в конструкторе класса, ему необходимо создавать экземпляр FilesystemDocumentRepository и передавать его.

Соглашения StructureMap

В примерах данной главы явно настраиваются StructureMap преобразования интерфейсов в типы посредством методов For и Use. Но StructureMap, в действительности, достаточно умен, чтобы самостоятельно разбираться в этих преобразованиях.

Вместо использования метода For мы могли бы также использовать метод Scan, чтобы разъяснить StructureMap, что ему необходимо просканировать все типы в конкретных сборках и попытаться определить, какие интерфейсы связать с какими классами:

ObjectFactory.Configure(cfg =>
{
	cfg.Scan(scan =>
	{
		scan.TheCallingAssembly();
		scan.WithDefaultConventions();
	});
});

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

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

Теперь, когда мы рассмотрели то, как мы можем использовать DI в обособленном примере, давайте перейдем к рассмотрению того, как мы можем использовать этот механизм в ASP.NET MVC приложении.