Feel Good.

29 апреля 2010

Hello, Unity 2.0

Рассматривая ранее принцип инверсии зависимости, мы остановились на том, что нам нужен некий IoC/DI фреймворк, который помог бы нам избавиться от рутинной работы. Как я уже говорил, существуем множество различных IoC/DI фреймворков:
  1. Microsoft Unity
  2. Ninject
  3. Autofac
  4. StructureMap
  5. Castle
  6. Spring.Net
В этой статье мы познакомимся с open-source IoC/DI контейнером от Microsoft Unity 2.0 под .NET 4.0. Работать с данным фреймворком очень легко, и я продемонстрирую это Вам на простом примере, проделав простых 5 шагов:
  1. Создадим новый проект и добавим в него основные сущности. Смотри предыдущую статью.
  2. Скачаем необходимые библиотеки.
  3. Добавим references на них в проекте.
  4. Настроим IoC-контейнер через конфигурационный XML-файл.
  5. С помощью IoC-контейнера получим экземпляр объекта нашего класса.

Первым делом создайте новый проект под именем UnityExample, в который добавьте, описанные в предыдущей статье, классы и интерфейсы в общее пространство имен UnityExample.
Вторым шагом, нам понадобиться сам контейнер Unity, его можно скачать в составе
Microsoft Enterprise Library 5.0 виде инсталляционного файла, установка которого производиться в режиме wizard installer и не требует наличия особых навыков, и в отличие от ручной установки, она автоматически зарегистрирует необходимые сборки в GAC добавит дополнение к Visual Studio. После установки добавим ссылки на необходимые библиотеки Unity:



  1. Microsoft.Practices.Unity.dll
  2. Microsoft.Practices.Unity.Configuration.dll
Далее можно приступать к работе.
Вообще, настраивать поведение IoC-контейнера можно и в коде, но все-таки хорошим тоном считается, если изменение конфигурации не приведет к изменению кода и повторной сборки проекта, поэтому всю конфигурацию мы вынесем в отдельный файл, причем не захламляя основной файл конфигурации App.config:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

 

    <configSections>

        <section

            name="unity"

            type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration" />

    </configSections>

 

    <!-- Вынесем все в отдельный файл -->

    <unity configSource="Unity.config" />

 

</configuration>


Сама настройка контейнера будет описана в файле Unity.config (не забудьте выставить опцию "Copy to Output Directory"). Следует отметить, что конфигурационный файл для Unity контейнера имеет XML-формат, при необходимости, если Ваш редактор XML файлов поддерживает XSD схемы и intellisense, то Вы можете упростить процесс конфигурирования, воспользовавшись XSD схемой (ставится вместе с Microsoft Enterprise Library 5.0) "c:\Program Files\Microsoft Visual Studio 9.0\xml\Schemas\UnityConfiguration20.xsd":

<?xml version="1.0" encoding="utf-8" ?>

 

<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">

 

    <!-- Заведем псевдонимы для известных типов -->

    <alias alias="IProcessor" type="UnityExample.IProcessor, UnityExample" />

    <alias alias="Processor" type="UnityExample.Processor, UnityExample" />

    <alias alias="Computer" type="UnityExample.Computer, UnityExample" />

 

    <container>

        <!-- Свяжем тип IProcessor с типом Processor -->

        <register type="IProcessor" mapTo="Processor"></register>

 

 

        <!-- Свяжем тип Computer с типом Computer -->

        <register type="Computer" mapTo="Computer">

            <constructor>

                <!-- Объявим аргумент конструктора processor типа IProcessor как зависимый -->

                <param name="processor" type="IProcessor">

                    <dependency/>

                </param>

            </constructor>

        </register>       

    </container>   

</unity>


Так как это все-таки ознакомительный пример, я решил поместить все сущности в один файл, в итоге получилось следующее:

using System;

using System.Configuration;

 

using Microsoft.Practices.Unity;

using Microsoft.Practices.Unity.Configuration;

 

namespace UnityExample

{

    /// <summary>

    /// Интерфейс процессора.

    /// </summary>

    interface IProcessor

    {

        /// <summary>

        /// Частота процессора.

        /// </summary>

        int ClockRate

        {

            get;

        }

    }

 

    /// <summary>

    /// Конкретный процессор с частотой 166 Мгц.

    /// </summary>

    class Processor : IProcessor

    {

        public int ClockRate

        {

            get

            {

                return 166;

            }

        }

    }

 

    /// <summary>

    /// Конкретный компьютер.

    /// </summary>

    class Computer

    {

        /// <summary>

        /// Процессор.

        /// </summary>

        protected IProcessor _processor;

 

        public Computer(IProcessor processor)

        {

            _processor = processor;

        }

 

        public void PrintDescription()

        {

            Console.Write("Clock rate: {0} Mhz", _processor.ClockRate);

        }

    }

 

    class Program

    {

        static void Main(string[] args)

        {

            // IoC контейнер.

            IUnityContainer container = new UnityContainer();

            // Загрузим конфигурацию для IoC контейнера из секции unity.

            UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");

            // Применим конфигурацию.

            section.Configure(container);

 

            // Получим экземпляр объекта типа Computer.

            Computer computer = container.Resolve<Computer>();                   

            computer.PrintDescription();

        }

    }

}


И если вы запустите проект, то Вы увидите на консоли сообщение "Clock rate: 166 Mhz", тем самым IoC-контейнер сам разрешил тип Computer, более того, контейнер автоматически разрешил зависимость Computer от IProcessor. Очень важно отметить, что мы НЕ использовали оператор new, который задал бы жесткую зависимость. Все это позволяет строить более гибкие архитектурные решения, которые способны полностью перестраиваться при небольшом изменении в конфигурационном файле.



В следующей статье я отвечу на популярные вопросы, касающиеся конфигурирования Microsoft Unity. Продолжение следует IoC/DI

18 комментариев:

  1. что то вы напустали здесь
    register type="Computer" mapTo="Computer"

    вероятно вы имели в виду

    register type="IComputer" mapTo="Computer"

    ОтветитьУдалить
  2. Нет, я для простоты не стал добавлять интерфейс IComputer, хотя можно и с ним (так даже правильно делать).

    А связка "register type="Computer" mapTo="Computer"" определяет self-binding.

    ОтветитьУдалить
  3. Если был бы интерфейс IComputer то, Вы правильно заметили, нужно было бы делать [register type="IComputer" mapTo="Computer"]

    Означало бы сконструировать тип Computer при разрешении общего типа IComputer.

    И соответственно изменился бы код:
    IComputer computer = container.Resolve[IComputer]();

    ОтветитьУдалить
  4. Все таки совсем не хорошо конфигурировать IoC через xml.
    Конфиг получается крайне сложный, отсутствует типизация, опечатка в нем приведет к падению нашего приложения. "любой залетевший дятел может разрушить цивилизацию".

    ОтветитьУдалить
  5. > Все таки совсем не хорошо конфигурировать IoC через xml.

    повод для "религиозных войн"))

    ОтветитьУдалить
  6. Help!

    IoC error: http://i48.tinypic.com/250qv6h.jpg

    ОтветитьУдалить
  7. @Arturo

    Надо добавить в проекте ссылке на пространство имен System.Configuration.

    ОтветитьУдалить
  8. Спасибо за статью.
    Пробую разобраться с Unity. Помогите с вопросами:
    1. В Вашем примере интерфейс (IProcessor) и классы (Processor,Program) находятся в одном неймспейсе UnityExample. Как настроить файл конфигурации если все в разных неймспейсах или в разных сборках. Зачем это нужно? Так понимаю, что container должен быть один на всё приложение, чтобы container.Resolve() выдал мне объект Computer в любом месте приложения где это необходимо (при ContainerControlledLifetimeManager - поведение singleton). Предположим, что Computer должен быть один и тот же в приложении.
    2. Как следствие из первого вопроса. Если будет много сборок со многими классы и интерфейсами, то как использовать Unity - он должен быть один на все приложение и знать о всех сборках (классы и интерфейсами, которые он "обслуживает"). Или как?
    Большое спасибо.

    ОтветитьУдалить
  9. С первым вопросом разобрался




    http://msdn.microsoft.com/en-us/library/dd203225.aspx

    ОтветитьУдалить
  10. @Slava
    Смотрите, к конфигурационном файле Вы просто указываете namespace разрешаемого типа. Либо, если Вы конфигурируете unity в runtime, просто делаете "add reference" необходимой сборки, а далее делаете доступными нужные namespace-ы через using.

    ОтветитьУдалить
  11. Так всё-же, будет продолжение?

    ОтветитьУдалить
  12. @Dzmuh

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

    ОтветитьУдалить
  13. Илья, как через xml сконфигурирвать в Unity singleton, чтобы он шарился для двух разных интерфейсов. Например
    class Impl : IA, IB {}







    Это не работает :(
    Unity создаёт разные инстансы

    ОтветитьУдалить
  14. блин
    <register type="Impl">
    <lifetime type="singleton" />
    </register>

    <register type="IA" mapTo="Impl"/>
    <register type="IB" mapTo="Impl"/>

    ОтветитьУдалить
  15. @aspnetmvcfan

    <?xml version="1.0" encoding="utf-8" ?>
    <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
    <!-- Заведем псевдонимы для известных типов -->
    <alias alias="IA" type="UnityTest.IA, UnityTest" />
    <alias alias="IB" type="UnityTest.IB, UnityTest" />
    <alias alias="AB" type="UnityTest.AB, UnityTest" />

    <container>

    <register type="IA" mapTo="AB">
    <lifetime type="singleton"/>
    </register>

    <register type="IB" mapTo="AB">
    <lifetime type="singleton"/>
    </register>

    </container>

    </unity>


    public interface IA
    {
    DateTime A { get; }
    }
    public interface IB
    {
    DateTime B { get; }
    }
    public class AB : IA, IB
    {
    readonly DateTime _now;

    public AB()
    {
    _now = DateTime.Now;
    }

    public DateTime A { get { return _now; } }
    public DateTime B { get { return _now; } }
    }

    class Program
    {
    static void Main(string[] args)
    {
    IUnityContainer c = new UnityContainer();
    UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
    section.Configure(c);


    IA a = c.Resolve[IA]();
    // Пауза...
    IB b = c.Resolve[IB]();

    //a.A.Equals(b.B)==true
    }
    }


    Возможно у вас пересоздается сам Unity контейнер.

    ОтветитьУдалить
  16. В итоге я плюнул на это и сконфигурировал через код :)
    Контейнер был один.

    ОтветитьУдалить
  17. У вас конструктор:
    public Computer(IProcessor processor)
    {
    _processor = processor;
    }


    а здесь вы объявляете экземпляр класса без аргумента:

    static void Main(string[] args){
    ...
    // Получим экземпляр объекта типа Computer.
    Computer computer = container.Resolve();
    computer.PrintDescription();
    }


    и как это работает?


    какую роль играет UnityContainer ?

    ОтветитьУдалить
  18. По шагам:
    1. Мы запрашиваем у UnityContainer экземпляр Computer (см метод Resolve)
    2. Computer имеет конструктор, который требует тип IProcessor, поэтому UnityContainer вынужден внутри себя найти подходящий тип IProcessor, чтобы подсунуть его в конструктор Computer.

    Суть в том, что мы запрашиваем у контейнера лишь экземпляр Computer, и при этом не заботимся о всех его зависимостях (в данном примере зависимость на IProcessor)

    Для полного понимания темы рекомендую книгу "Dependency Injection in .NET" автора Mark Seemann (http://www.amazon.com/Dependency-Injection-NET-Mark-Seemann/dp/1935182501)

    ОтветитьУдалить