Главная страница   /   3.4. Строительство слабосвязанных компонентов (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

3.4. Строительство слабосвязанных компонентов

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

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

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

Любой другой компонент нашего приложения, которому нужно отправить имейл, скажем, например, вспомогательный метод сброса пароля PasswordResetHelper, может отправить электронную почту, только ссылаясь на методы интерфейса. Как показано на рисунке 3-7, между PasswordResetHelper и MyEmailSender не существует прямой зависимости.

Рисунок 3-7: Использование интерфейсов для разделения компонентов

Вводя IEmailSender, мы гарантируем, что не существует прямой зависимости между PasswordResetHelp и MyEmailSender. Мы могли бы заменить MyEmailSender другим компонентом для отправки электронной почты или даже использовать в целях тестирования mock-объекты. Мы вернемся к теме mock-объектов в главе 6.

Примечание

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

Использование внедрения зависимости (Dependency Injection)

Интерфейсы помогают нам разделять компоненты, но у нас по-прежнему есть проблема: C# не предоставляет встроенного способа для легкого создания объектов, реализующих интерфейсы, за исключением создания интерфейсов конкретного компонента. В конечном итоге, у нас есть код, показанный в листинге 3-3.

Листинг 3-3: Создание экземпляра конкретного класса для осуществления реализации интерфейса
public class PasswordResetHelper {
	public void ResetPassword() {
		IEmailSender mySender = new MyEmailSender();
		//...вызов методов интерфейса для конфигурации информации по имейлу
		mySender.SendEmail();
	}
}

Мы всего лишь частично на пути к слабосвязанным компонентам: класс PasswordResetHelper настраивает и отправляет электронную почту через интерфейс IEmailSender, но для создания объекта, который реализует этот интерфейс, он должен был создать экземпляр MyEmailSender.

Мы сделали еще хуже, теперь PasswordResetHelper зависит от IEmailSender и MyEmailSender, как показано на рисунке 3-8.

Рисунок 3-8: Компоненты, которые все равно тесно связаны

Что нам нужно, так это способ получить объекты, которые реализуют данный интерфейс без необходимости создания объекта напрямую. Решение этой проблемы называется внедрением зависимости (Dependency injection, DI), известное также как инверсия управления (Inversion of Control, IoC).

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

Есть два варианта DI паттерна. Первый состоит в том, что мы удаляем все зависимости от конкретных классов из наших компонентов, в данном случае PasswordResetHelper. Это делается путем передачи реализации необходимых интерфейсов в конструктор класса, как показано в листинге 3-4.

Листинг 3-4: Удаление зависимостей из класса PasswordResetHelper
public class PasswordResetHelper
{
	private IEmailSender emailSender;

	public PasswordResetHelper(IEmailSender emailSenderParam)
	{
		emailSender = emailSenderParam;
	}

	public void ResetPassword()
	{
		// ...вызов методов интерфейса для конфигурации информации по имейлу...
		emailSender.SendEmail();
	}
}

Мы сломали зависимость между PasswordResetHelper и MyEmailSender: конструктор PasswordResetHelper требует объект, который реализует интерфейс IEmailSender, но он не знает, или его не волнует, что это за объект и он больше не ответственен за его создание.

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

Примечание

Класс PasswordResetHelper требует того, чтобы его зависимости были внедрены с помощью его конструктора – это известно как constructor injection (внедрение через конструктор). Мы могли бы также сделать так, чтобы зависимости были внедрены через открытые свойства, что известно как setter injection (внедрение через свойства).

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

DI пример с MVC

Давайте вернемся к аукционной доменной модели, которую мы создали ранее, и применим к ней DI. Целью является создание класса контроллера, мы назовем его AdminController. Он использует репозиторий MembersRepository, но без непосредственной связи AdminController и MembersRepository. Мы начнем с определения интерфейса, который разделит наши два класса – мы назовем его IMembersRepository – и изменим класс MembersRepository для реализации интерфейса, как показано в листинге 3-5.

Листинг 3-5: Интерфейс IMembersRepository
public interface IMembersRepository
{
	void AddMember(Member member);
	Member FetchByLoginName(string loginName);
	void SubmitChanges();
}

public class MembersRepository : IMembersRepository
{
	public void AddMember(Member member)
	{
		/* Реализуй меня */
	}
	public Member FetchByLoginName(string loginName)
	{
		/* Реализуй меня */
	}
	public void SubmitChanges()
	{
		/* Реализуй меня */
	}
}

Теперь мы можем написать класс контроллера, который зависит от интерфейса IMembersRepository, как показано в листинге 3-6.

Листинг 3-6: Класс AdminController
public class AdminController : Controller
{
	IMembersRepository membersRepository;

	public AdminController(IMembersRepository repositoryParam)
	{
		membersRepository = repositoryParam;
	}

	public ActionResult ChangeLoginName(string oldLoginParam, string newLoginParam)
	{
		Member member = membersRepository.FetchByLoginName(oldLoginParam);
		member.LoginName = newLoginParam;
		membersRepository.SubmitChanges();
		// ... теперь будет показано представление
		return View();
	}
}

Класс AdminController требует реализации интерфейса IMembersRepository в качестве параметра конструктора: он будет внедрен во время выполнения, что позволяет AdminController работать на экземпляр класса, реализующего интерфейс, не будучи связанным с этой реализацией.

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

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

Ответом является контейнер внедрения зависимостей, также известный как IoC контейнер. Это компонент, который действует в качестве посредника между зависимостями, которые требует класс типа PasswordResetHelper, и конкретной реализацией этих зависимостей, как MyEmailSender.

Мы регистрируем набор интерфейсов или абстрактных типов, которые использует наше приложение, с DI контейнером, и говорим ему, для каких конкретно классов должны быть созданы экземпляры для удовлетворения зависимостей. Таким образом, мы регистрируем интерфейс IEmailSender с контейнером и указываем, что должен быть создан экземпляр MyEmailSender всякий раз, когда требуется реализация IEmailSender. Когда бы нам ни потребовался IEmailSender, например, для создания экземпляра PasswordResetHelper, мы идем к DI контейнеру, и нам дается реализация класса, который мы зарегистрировали в качестве конкретной реализации по умолчанию этого интерфейса – в данном случае, MyEmailSender.

Нам не нужно создавать DI контейнеры самим: есть несколько отличных версий с открытым исходным кодом и свободной лицензией. Один из контейнеров, который нам нравится, называется Ninject, и вы можете получить информацию о нем на www.ninject.org. Мы познакомим вас с использованием Ninject в главе 6.

Совет

Microsoft создал свой собственный DI контейнер, который называется Unity. Однако, мы собираемся использовать Ninject, потому что нам он нравится и он демонстрирует способность смешивать и сочетать инструменты при использовании MVC. Если вы хотите получить больше информации о Unity, посетите unity.codeplex.com.

Роль контейнера DI может показаться простой и тривиальной, но это не тот случай. Хороший DI контейнер, такой как Ninject, обладает некоторыми очень умными функциями:

  • Цепочка зависимостей: Если вы запрашиваете компонент, который имеет свои собственные зависимости (например, параметры конструктора), контейнер также будет удовлетворять эти зависимости. Таким образом, если конструктор для класса MyEmailSender требует применения интерфейса INetworkTransport, DI контейнер подтвердит реализацию по умолчанию этого интерфейса, передаст его конструктору MyEmailSender и вернет результат в виде реализации по умолчанию IEmailSender.
  • Управление жизненным циклом объекта: Если вы запрашиваете компонент более чем один раз, должны ли вы каждый раз получать тот же экземпляр или новый? Хороший DI контейнер позволит вам настроить жизненный цикл компонентов, разрешая выбрать один из предопределенных вариантов, включая singleton (тот же экземпляр каждый раз), transient (новый экземпляр каждый раз), instance-per-thread, instance-per-HTTP-request, instance-from-a-pool и многие другие.
  • Конфигурация значений параметров конструктора: Например, если конструктор для нашей реализации интерфейса INetworkTransport требует строки serverName, вы в состоянии установить значение для нее в конфигурации DI контейнера. Это грубая, но простая система конфигурации, которая удаляет любую необходимость для вашего кода обходить строки подключения, адреса серверов и так далее.

Возможно, вы попытаетесь написать свой собственный DI контейнер. Мы считаем, что это большой экспериментальный проект, если у вас есть время, которое вы можете убить, и вы хотите узнать много нового о C# и .NET отражении (reflection). Если же вы хотите использовать DI контейнер в MVC приложении для производственных целей, мы рекомендуем вам воспользоваться одним из зарекомендовавших себя DI контейнеров, таким как Ninject.