Цель урока: Изучение DI (Dependency Injection). Пример на Ninject и Unity (Autofac, Winsor).
Во многих случаях, один и тот же экземпляр класса используется в вашем приложении в разных модулях. Простым способом реализации является применение шаблона Одиночка (Singleton).
Но рассмотрим эту ситуацию с другой стороны. Так как данный объект создается при первом обращении к нему, мы не можем контролировать его время жизни. При модульном тестировании (unit-test) нет необходимости использовать этот объект (или это может быть невозможно). Чтобы избежать этого, мы не напрямую вызываем объект, а через интерфейс. И реальный экземпляр класса, и экземпляр-заглушка для тестирования будут реализовывать этот интерфейс. А логику создания мы поручаем DI-контейнеру.
Например, до использования сервиса. Опишем пару классов, интерфейс IWeapon с методом Kill, два класса реализации Bazuka и Sword, и класс Warrior, который пользуется оружием:
public interface IWeapon
{
void Kill();
}
public class Bazuka : IWeapon
{
public void Kill()
{
Console.WriteLine("BIG BADABUM!");
}
}
public class Sword : IWeapon
{
public void Kill()
{
Console.WriteLine("Chuk-chuck");
}
}
public class Warrior
{
readonly IWeapon Weapon;
public Warrior(IWeapon weapon)
{
this.Weapon = weapon;
}
public void Kill()
{
Weapon.Kill();
}
}
Используем это:
class Program
{
static void Main(string[] args)
{
Warrior warrior = new Warrior(new Bazuka());
warrior.Kill();
Console.ReadLine();
}
}
Читаем между строк. Создаем воина и даем ему базуку, он идет и убивает. В консоли получаем:
BIG BADABUM!
Заметим, что у нас нет проверки на null в строке
Weapon.Kill();
Что здесь некоректно? Воин не знает, есть ли у него оружие, и выдачей оружия занимается не отдельный модуль, а главная программа.
Суть DI – поручить выдачу оружия другому модулю.
Подключаем Ninject:
Install-Package Ninject
Создаем модуль, который занимается выдачей оружия:
public class WeaponNinjectModule : NinjectModule
{
public override void Load()
{
this.Bind<IWeapon>().To<Sword>();
}
}
Что буквально значит: «если попросят оружие – то выдайте мечи».
Создаем «сервис-локатор» и пользуемся оружием:
class Program
{
public static IKernel AppKernel;
static void Main(string[] args)
{
AppKernel = new StandardKernel(new WeaponNinjectModule());
var warrior = AppKernel.Get<Warrior>();
warrior.Kill();
Console.ReadLine();
}
}
Как видно, объект warrior мы создаем не с помощью конструкции new, а через AppKernel.Get<>()
. При создании AppKernel, мы передаем в качестве конструктора модуль, отвечающий за выдачу оружия (в данном случае это меч). Любой объект, который мы пытаемся получить через AppKernel.Get
, будет (по мере возможности) проинициализирован, если существуют модули, которые знают, как это делать.
Другой момент применения, когда объект Warrior
не берет с собой оружие каждый раз, а при не обнаружении оного обращается к сервису локатору и получает его:
public class OtherWarrior
{
private IWeapon _weapon;
public IWeapon Weapon
{
get
{
if (_weapon == null)
{
_weapon = Program.AppKernel.Get<IWeapon>();
}
return _weapon;
}
}
public void Kill()
{
Weapon.Kill();
}
}
Исполняем:
var otherWarrior = new OtherWarrior();
otherWarrior.Kill();
Наш воин получает оружие по прямым поставкам – супер!
В Ninject есть еще одна очень хорошая деталь. Если свойство (public property
) помечено [Inject]
, то при создании класса через AppKernel.Get<>()
– поле инициализуется сервисом-локатором:
public class AnotherWarrior
{
[Inject]
public IWeapon Weapon { get; set; }
public void Kill()
{
Weapon.Kill();
}
}
var anotherWarrior = AppKernel.Get<AnotherWarrior>();
anotherWarrior.Kill();
Unity
Абсолютно всё то же:
- Установка
Install-Package Unity
- Инициализация сервиса локатора (Container)
Container = new UnityContainer();
- Регистрация типа
Container.RegisterType(typeof(IWeapon), typeof(Bazuka));
- Получение объекта и использование:
var warrior = Container.Resolve<Warrior>(); warrior.Kill();
- Кроме того, у Unity есть класс-одиночка
(Singleton) ServiceLocator
, который регистрирует контейнер и позволяет получить доступ к сервисам из любого места.var serviceProvider = new UnityServiceLocator(Container); ServiceLocator.SetLocatorProvider(() => serviceProvider);
- Хитрый
OtherWarrior
теперь так получает оружие:public class OtherWarrior { private IWeapon _weapon; public IWeapon Weapon { get { if (_weapon == null) { _weapon = ServiceLocator.Current.GetInstance<IWeapon>(); } return _weapon; } } public void Kill() { Weapon.Kill(); } }
Autofac
Так же, собственно, всё и происходит:
- Установка
Install-Package Autofac
- Инициализация строителя сервиса-локатора (
ContainerBuilder
) – нет-нет, это еще не сам контейнер, это — как модулиvar builder = new ContainerBuilder();
-
Регистрация типов. Надо зарегистрировать все необходимые классы, потому что создание экземпляров незарегистрированных классов тут не реализован.
builder.RegisterType<Bazuka>(); builder.RegisterType<Warrior>(); builder.Register<IWeapon>(x => x.Resolve<Bazuka>());
- Создание сервиса локатора (Container)
var container = builder.Build();
- Получение объекта и использование:
var warrior = container.Resolve<Warrior>(); warrior.Kill();
Castle Windsor
- Установка
Install-Package Castle.Windsor
- Инициализация сервиса-локатора
var container = new WindsorContainer();
- Регистрация типов. Аналогична как и в Autofac.
container.Register(Component.For<IWeapon>().ImplementedBy<Bazuka>(), Component.For<Warrior>().ImplementedBy<Warrior>());
- Получение объекта и использование:
var warrior = container.Resolve<Warrior>(); warrior.Kill();
Маленький подитог
На самом деле, реализации Dependency Injection не сильно, но всё же отличаются. Некоторые поддерживают инициализацию в Web.config (App.config)
файлах. Некоторые, задают правила для инициализации, как мы сейчас посмотрим на расширении Ninject для asp.net mvc – это касается инициализации сервиса-локатора как генератора общих объектов, так и отдельно для каждого потока или web-запросе.
Объекты областей (Ninject)
В Ninject можно задать несколько способов инициализации получения объекта из класса. Если мы работаем в различных контекстах (например, в разных потоках (Thread)), то объекты должны быть использованы разные. Тем самым, поддерживается масштабируемость и гибкость приложения.
Область | Метод связывания | Объяснение |
Временный | .InTransientScope() |
Объект класса будет создаваться по каждому требованию (метод по умолчанию). |
Одиночка | .InSingletonScope() |
Объект класса будет создан один раз и будет использоваться повторно. |
Поток | .InThreadScope() |
Один объект на поток. |
Запрос | .InRequestScope() |
Один объект будет на каждый web-запрос |
Lifetime Manager в Unity
В Unity для задачи правил инициализации используется реализация абстрактного класса LifetimeManager.
Происходит это так:
_container.RegisterType<DbContext, SavecashTravelContext>(new PerRequestLifetimeManager());
Где PerRequestLifetimeManager – это реализация LifetimeManager:
public class PerRequestLifetimeManager : LifetimeManager
{
/// <summary>
/// Key to store data
/// </summary>
private readonly string _key = String.Format("SingletonPerRequest{0}", Guid.NewGuid());
/// <summary>
/// Retrieve a value from the backing store associated with this Lifetime policy.
/// </summary>
/// <returns>
/// the object desired, or null if no such object is currently stored.
/// </returns>
public override object GetValue()
{
if (HttpContext.Current != null && HttpContext.Current.Items.Contains(_key))
return HttpContext.Current.Items[_key];
return null;
}
/// <summary>
/// Stores the given value into backing store for retrieval later.
/// </summary>
/// <param name="newValue">The object being stored.</param>
public override void SetValue(object newValue)
{
if (HttpContext.Current != null)
HttpContext.Current.Items[_key] = newValue;
}
/// <summary>
/// Remove the given object from backing store.
/// </summary>
public override void RemoveValue()
{
if (HttpContext.Current != null && HttpContext.Current.Items.Contains(_key))
HttpContext.Current.Items.Remove(_key);
}
}
Суть. Все объекты хранятся в HttpContext.Current.Items[_key]
и выдаются только, если уже находятся в том же контексте (HttpContext.Current
). В ином случае, создается новый объект. Если текущий контекст (HttpContext.Current
) в области кода не существует (используем такой LifetimeManager
в консольном приложении или в отдельном потоке) – то данный контейнер не будет работать.
Использование Ninject в asp.net mvc
Устанавливаем Ninject в среду asp.net mvc. Отдельно создаем свой проект LessonProject, создадим там HomeController с методом и view Index. (/Contollers/HomeController.cs):
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
И (/Views/Home/Index.cshtml):
@{
ViewBag.Title = "LessonProject";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>LessonProject</h2>
Запускаем – работает.
Примечание: В дальнейшем мы будем переносить этот проект в последующие уроки.
Теперь установим модуль Ninject и Ninject.MVC3 для этого проекта.
Install-Package Ninject.MVC3
Добавляем класс в папку App_Start (/App_Start/NinjectWebCommon.cs):
[assembly: WebActivator.PreApplicationStartMethod(typeof(LessonProject.App_Start.NinjectWebCommon), "Start")]
[assembly: WebActivator.ApplicationShutdownMethodAttribute(typeof(LessonProject.App_Start.NinjectWebCommon), "Stop")]
namespace LessonProject.App_Start
{
using System;
using System.Web;
using Microsoft.Web.Infrastructure.DynamicModuleHelper;
using Ninject;
using Ninject.Web.Common;
public static class NinjectWebCommon
{
private static readonly Bootstrapper bootstrapper = new Bootstrapper();
/// <summary>
/// Starts the application
/// </summary>
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
bootstrapper.Initialize(CreateKernel);
}
/// <summary>
/// Stops the application.
/// </summary>
public static void Stop()
{
bootstrapper.ShutDown();
}
/// <summary>
/// Creates the kernel that will manage your application.
/// </summary>
/// <returns>The created kernel.</returns>
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
return kernel;
}
/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
}
}
}
В RegisterServices мы добавляем инициализацию своих сервисов. Для начала добавим шутливый IWeapon, а в дальнейшем еще будем возвращаться к этому методу для регистрации других сервисов:
public interface IWeapon
{
string Kill();
}
…
public class Bazuka : IWeapon
{
public string Kill()
{
return "BIG BADABUM!";
}
}
…
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IWeapon>().To<Bazuka>();
}
В контроллере используем атрибут [Inject]
:
public class HomeController : Controller
{
[Inject]
public IWeapon weapon { get; set; }
public ActionResult Index()
{
return View(weapon);
}
}
Изменяем View:
@model LessonProject.Models.IWeapon
@{
ViewBag.Title = "LessonProject";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>LessonProject</h2>
<p>
@Model.Kill()
</p>
На выходе получаем:
Ninject использует WebActivator:
- регистрирует свои модули OnePerRequestHttpModule и NinjectHttpModule
- создает StandartKernel
- инициализирует наши сервисы.
DependencyResolver
В asp.net mvc3 появился класс DependencyResolver. Этот класс обеспечивает получение экземпляра сервиса. Наши зарегистрированные сервисы (и даже используемый DI-контейнер) мы также можем получить посредством этого класса.
public class HomeController : Controller
{
private IWeapon weapon { get; set; }
public HomeController()
{
weapon = DependencyResolver.Current.GetService<IWeapon>();
}
public ActionResult Index()
{
return View(weapon);
}
}
Итог
Использование DI-контейнеров в современных приложениях необходимо, чтобы избавиться от сильной связности кода, и для легкого доступа из любой его части к сервисам. Также, это необходимо для написания Unit-тестов.
Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons
Автор: chernikov