Пользователь
0,0
рейтинг
9 апреля 2013 в 22:56

Разработка → ASP.NET MVC Урок 2. Dependency Injection tutorial

ASP*, .NET*
Цель урока: Изучение 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 @chernikov
карма
177,9
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (15)

  • +3
    Всё-таки предпочтительным вариантом для получения экземпляра является конструктор. Использование Service Locator оправдано только там, где без этого совсем не обойтись(Например, в фильтрах, но даже и там возможны варианты).
  • 0
    DI через проперти семантически отличается от DI через конструктор тем, что инъекция вобщем-то может и не произойти, или, например, объект может быть изменен после создания. К тому же, вне IoCC окружения, этот модуль необходимо будет создавать особым образом, помня, какие проперти обязательны к установке, а какие нет. Это частный случай Sequential coupling, что является антипаттерном.

    Вы как бы пытаетесь развязать зависимости модулей друг от друга, но в то же время жестко подвязываетесь на сам Service Locator. Теряется реюзабельность. Выходит, что модули без определенного и конкретного Service Locator'a существовать не могут, придется воссоздавать окружение. И именно поэтому использование Service Locator'a тоже является антипаттерном. Но, конечно, есть случаи, где по-другому никак, но не в этих примерах в статье.

    По этим причинам предпочтительнее делать инъекцию обязательных зависимостей через конструктор в readonly поля. Либо, в случае инъекций в проперти, обеспечить иммутабельность object reference для зависимости и потокобезопасное выполнение. Но это же ненужные сложности. :)
  • +1
    Довольно сложный материал для 2 урока, не усвоил зачем нужен Service Locator
    • +1
      Суть Service Locator — развязать зависимости модулей.

      Зачем это нужно? У нас есть границы модуля. Например примерно так будет выглядеть стандартное MVC приложение:


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

      Чтобы проверять программно, мы должны все окружающие его модули. Т.е. всё окружение мы меняем на объекты-заглушки: HttpContext, IRepository, Mail, SmsProvider и др. Т.е. всё окружение — это не реальные модули, а декорации.

      Для чего это надо? Если мы снимаем фильм, легче снимать в павильоне, мы восстанавливаем комнату и актеры дубль за дублем играют роль. Если необходима уличная съемка — мы или восстанавливаем в павильоне часть улицы или выходим на улицу. Иногда декорации могут быть дорогостоящие, как в «Шоу Трумена», чтобы актер мог лучше сыграть. Тестирование объекта (в нашем случае контроллера) — это и есть проигрывание сценария.

      Если инициализация (вызов конструктора) будет находится в самом контроллере мы не сможем заменить реальное окружение на декорации не изменяя кода. Для этого мы и используем Service Locator.

      Больше его суть раскрывается в уроке E, где мы активно начинаем использовать mock-объекты, переделываем NotifyMail (из урока A) под сервис и используем интегрированное тестирование (где тестируем и реальный код SqlRepository).

  • 0
    у меня такой вопрос: какой контейнер IoC вы сами чаще всего используете и почему?
    Судя по этому уроку и уроку Е — всё-таки Ninject. От чего же?
    • 0
      Личные предпочтения.
      • 0
        не сочтите за приставалу, а как изначально к нему пришли? Просто вижу, что владеете и другими реализациями принципа IoC, и погуглив и наткнувшись сравнение нескольких контейнеров в т.ч. по перфомансу, увидел, что Ninject далеко не самый лучший, но, вероятно, наиболее удобный
        • 0
          Изначально пользовался им, но когда дошло дело до статей, рассмотрел и другие IoC-контейнеры. Пользовался еще на чужом проекте Unity, но Ninject просто лучше знаю. По производительности, удобству — не сравнивал.
  • 0
    Добрый день,

    Я недавно ознакомился с трудом Seemann Mark под названием Dependency Injection in .NET. Не знаю какой вес имеет данный автор в мире .net но меня удивил тот факт что он прямо таки приравнял Service Locator к анти-паттернам. Признаюсь я использую в моих проектах юнити в купе с паттерном Service Locator но как-то не совсем представляю как обойтись без оного или чем заменить. В своих примерах автор часто создает экземпляр контейнера, для объяснения это подходит но для реал-ворлд приложения не очень. Что скажет автор по данному поводу? Спасибо
    • 0
      Я немного запутался, какой автор — это я, а какой автор — это Seemann Mark.
      Я просто расскажу, как я считаю. Если вы пишете приложение, то можно обойтись без DI, изначально. Но потом — ближе к глобальным изменениям проекта наступает кризис. Мы изменяем что-то. Другой программист изменяет что-то. И всё перестает работать. Надо тестирование. Для тестирования необходим DI. В некоторых случаях этот момент может и не произойти. Вы сделали проект, его оплатили (или не оплатили) и всё это DI-использование повисло в воздухе. Оно не сыграло свою положительную роль. Мои проекты доживают до стадии глобальных изменений. DI мне нужен, заказчики платят деньги. Все довольны.
      Но (!) возможно Seemann Mark пошел дальше, он участвовал в проекте и в какой-то неясной ситуации достиг критической точки. И сидя дома в думах понял глобальную ошибку — «не надо было использовать Service Locator». И написал по этому поводу книгу.
      Я не отношусь к тому типу людей, которые говорят: «Это используйте, это хорошо. Это не используйте, это плохо». Я думаю, что важно знать инструменты и уметь оценивать риск их использования, и соответственно применять.
    • +1
      Попробую внести ясность, мне вообще не понравилось, что автор этой статьи склоняет к использованию Service Locator'a.

      Service Locator — это не DI в смысле Dependency Injection, а это DI в смысле Dependency Inversion.

      Dependency Injection — Это подача зависимости классу извне. Простыми словами звучит так: не инстанциируй сущности, не предназначенные для инстанцииорвания сущностей, внутри других сущностей, но если нужно инстанциирование сущностей — используй для этого специализированные сущности (фабрики, например).
      Сервис-локатор же получает некий сервис «изнутри» класса. Он как бы говорит, что «мне нужно это, дайте немедленно». Хорошим подходом же является декларативное объявление требований зависимостей. Не используя никакие сервис локаторы, просто объявляем в конструкторе, что нам нужно. Для решения таких зависимостей нам поможет Inversion of Control Contrainer. Ninject, кстати, им является.

      Dependency Inversion — это связывание модулей нижнего уровня на высоком уровне. Одним из вариантов которого является связывание реализаций при помощи интерфейсов. Поэтому Service Locator можно к этому отнести.

      Service Locator является Dependency Inversion, Dependency Injection является Dependency Inversion, но Service Locator это не Dependency Injection.

      Когда вы используете Service Locator внутри сущности, вы отвязываетесь от других зависимостей по интерфейсу. Но при этом привязываетесь реализацией к сервис-локатору. Это не является декаплингом (decoupling) зависимостей, и прежде всего поэтому это антипаттерн. Подробнее можно почитать на википедии, например. Английской, конечно же.

      Наиболее удобным путем внедрения зависимостей является использование Inversion of Control контейнера, коим и является Ninject. Когда вы используете IoCC, вы не используете в своих классах некие сомнительные сторонние сервисы, синглторы, депенденси резолверы и сервис-локаторы. IoCC позволяют настраивать время жизни объектов. Они сами решают, что нужно запрашиваемым сущностям, и делают это в рантайме.

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

      Мне из всех IoCC контейнеров под .NET больше всего нравится Castle Windsor. Он очень чистый, не мешается с вашим кодом и постоянно развивается. Можете почитать пример внедрения Castle Windsor в ваше ASP.NET MVC приложение.
      • 0
        Спасибо за исчерпывающий ответ. Теперь увидел разницу. Если мне не изменяет память то в плане юнити использование IoC например по средствам работы с конструкторами по сравнению с Service Locator будет немного более медленным особенно там где будет присутствовать вложенная инициализация. Но в целом подход намного чище чем Service Locator.
        • 0
          Быстродействие зависит от самого IoCC. Castle Windsor, например, узнает требуемые зависимости через рефлексию. В Autofac же, их нужно прописывать явно. Конечно, в этом случае Autofac будет быстрее, но все равно это не bottleneck в приложении. :)
  • 0
    ещё вопрос, вероятно глупый: почему Autofac и Castle вынесены в скобки после Unity? Разве все реализации не самодостаточны?
    • 0
      Самодостаточны, просто я с ними не работал и это как бы внутренне для меня они второстепенны. Поправил.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.