DI и IoC для начинающих

    Тема DI/IoC достаточно простая, но в сети очень сложно найти хорошее описание того, как это работает и зачем это нужно. Вот моя попытка, с использованием Unity. Хорошо ли объяснена тема – судить вам.


    Давайте создадим простенький пример:

    // сервис. содержит бизнес-логику
    public class MyService<br/>
    {<br/>
      public MyService()<br/>
      {<br/>
        ⋮<br/>
      }<br/>
      public void DoSomething()<br/>
      {<br/>
        ⋮<br/>
      }<br/>
    }<br/>
     <br/>
    // окошко. пользуется бизнес-логикой
    public class MyWindow : Form<br/>
    {<br/>
      private MyService service;<br/>
      public MyWindow()<br/>
      {<br/>
        service = new MyService();<br/>
      }<br/>
    }<br/>
     <br/>
    // програмка которая показывает окошко
    public class MyProgram<br/>
    {<br/>
      static void Main()<br/>
      {<br/>
        ⋮<br/>
        /// создаем окно
        Application.Run(new MyWindow);<br/>
      }<br/>
    }<br/>
    В этом коде одна проблема: класс MyWindow слишком сильно привязан к классу MyService. Это плохо потому что

    • Не получится написать unit-тест для MyWindow в отрыве от MyService. Фактически получится интеграционный тест.
    • Невозможно заменить MyService на OtherService, если только OtherService от него не наследует. Если от MyService зависят несколько классов, придется менять их все.
    • Если наш сервис требует настройки, его придется настраивать в каждом классе, который его использует.

    Если коротко, то наша проблема – это operator new(). Чтобы грамотно контролировать зависимости и позволить себе тестировать объекты в изоляции, от этого оператора нужно отказаться.

    Как? Этим-то как раз и занимаются паттерны IoC и DI. IoC (Inversion of Control) – это паттерн, в котором управление объектом (в нашем случае – временем жизни объекта) поручено какой-то компоненте. Некий такой аутсорс – вместо того чтобы создавать объект самим (через new()) мы запрашиваем его у т.н. IoC-контейнера, то есть у фабрики, которая умеет грамотно производить объекты.

    Правда каждый раз просить копию объекта у контейнера бывает лень. В этих случаях мы можем воспользоваться другим паттерном. DI (Dependency Injection) позволяет нам автоматически вытянуть из контейнера нужные нам зависимости при инициализации. То есть, когда мы создаем MyWindow через IoC-контейнер, механизм DI магическим способом сможет инициализировать MyService без нашего прямого участия.

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


    Воспользуемся фреймкорком Unity для нашей программы. Для начала, перепишем Main() – создадим в нем контейнер и применим DI к нашему окошку:

    public class MyProgram<br/>
    {<br/>
      static void Main()<br/>
      {<br/>
        ⋮<br/>
        var uc = new UnityContainer();<br/>
        Application.Run(uc.Resolve<MyWindow>());<br/>
      }<br/>
    }<br/>
    Методом Resolve() контейнера, мы запрашиваем не только создание объекта типа MyWindow, но и автоматической создание всех его зависимостей. Теперь посмотрим на то, как можно получить создание сервиса (т.е. зависимой части) автоматически. Для начала, вытащим интерфейс сервиса, чтобы его можно было в последствии поменять:

    interface IService<br/>
    {<br/>
      void DoSomething();<br/>
    }<br/>
     <br/>
    public class MyService : IService<br/>
    {<br/>
      ⋮ // все как и раньше
    }<br/>
    Теперь меняем MyWindow чтобы использовался именно интерфейс. Есть несколько вариантов того, как можно добавить ссылку на сервис чтобы контейнер его инициализировал. Вот один из них:

    public class MyWindow : Form<br/>
    {<br/>
      private IService service;<br/>
      public MyWindow(IService service)<br/>
      {<br/>
        this.service = service;<br/>
      }<br/>
    }<br/>
    Теперь осталось сделать только одно – сказать контейнеру чтобы по запросу объектов типа IService он выдавал MyService:

    public class MyProgram<br/>
    {<br/>
      static void Main()<br/>
      {<br/>
        ⋮<br/>
        var uc = new UnityContainer();<br/>
        uc.RegisterType<IService, MyService>();<br/>
        Application.Run(uc.Resolve<MyWindow>());<br/>
      }<br/>
    }<br/>
    Вот и все!!! Теперь при запуске программы переменная service окошка будет инициализирована автоматически. Это называется «инъекция в конструктор» (constructor injection). Хотите оставить конструктор пустым? Пожалуйста:

    public class MyWindow : Form<br/>
    {<br/>
      [Dependency]<br/>
      public IService Service { get; set; }
    <br/>
     <br/>
      public MyWindow()<br/>
      {<br/>
        ⋮<br/>
      }<br/>
    }<br/>
    Сменив поле на свойство и пометив его аттрибутом [Dependency], мы намекнули контейнеру, что его нужно инициализировать при создании класса. Результат тот же, что и с конструктором. Такой прием называется «инъекция в свойство» (setter injection).

    Вот и все! Спасибо за внимание! Если вас заинтересовала тема и вы живете в Петербурге, приходите к нам завтра на встречу.

    Читать 2ю часть серии

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

    Комментарии 46

      0
      Завтра продолжим эту тему, хочется услышать об опыте коллег, а так же сравнить существующие контейниры
        0
        Я делал сравнение (на английском), будет интересно почитать что изменилось.
          0
          Да, кстате, спасибо за обзор в вашем блоге.
          0
          Итак, какие у нас выводы? Unity, судя по всему, на порядок проще Spring и намного лучше подходит для обучения, имхо. Автопроксиинг всех объектов контейнера — зачем? Когда это действительно нужно? =)
          0
          Не плохо. я этого не знал. Но не хватает примера по-серьёзнее (из практики например).
            +1
            Получается, что все объекты, определённые как имеющие интерфейс IService, будут инициализироваться экземпляром MyService? Или это пример так подобран?

            Другими словами, если мне необходимо иметь 2 объекта с интерфейсом IService в одном MyWindow, то фреймворк позволит это сделать?
              0
              Конечно! Система предельно гибкая. Можно написать например:
              public class MyWindow
              {
               public IService firstService, secondService;
               public MyWindow(IService first, IService second)
               {
                firstService = first;
                secondService = second;
               }
              }
              Только не забудьте, что by default, контейнер выдает синглтоны. Чтобы получить разные копии сервисов, нужно слегка изменить регистрацию:
              var uc = new UnityContainer();
              uc.RegisterType<IService, MyService>(new ExternallyControlledLifetimeManager());
              Application.Run(uc.Resolve<MyWindow>());
                0
                Я так понимаю спрашивалась ситуация, когда будет:
                public class MyFirstServiceImpl : IService{}
                public class MySecondServiceImpl : IService{}
                  0
                  Тогда есть сразу несколько вариантов ответа :) Например использовать InjectorProperty. Или добавлять marker interface для обоих сервисов. Или декларировать их как конкретные типы. Или задать им имена и резолвить вручную, по имени. (но это уже не DI)
                  0
                  Ха! Ошибся. Не знаю что в голову ударило… конечно дефолтное поведение не синглтон :) Сорри!
                  0
                  Вопрос в том различаются эти обекты, или нет.
                  По логике, если они имплементируют один и тот же интерфейс, то и использование у них будет однородное.

                  В таком случае параметр можно заменить на IService[] services и дальше работать со всеми имплементациями сразу.
                    0
                    Это тоже, кстати, вариант.
                • НЛО прилетело и опубликовало эту надпись здесь
                    +1
                    Другие контейнеры тоже хороши. Правда, насколько я помню, Spring поддерживает только конфигурацию в XML, что лично мне не нравится. Обычно происходит так: с чем разработчик начинает работаеть, то и использует. Разница во фреймворках редко переманивает его с одного на другого.

                    Примеров актуального использования контейнеров в сети — миллион. Один из примеров — когда при неправильно работающем приложении нужно заменить обычную реализацию на динамический прокси с детальным логгированием. Без перекомпиляции. На живой системе. Тут-то внешняя конфигурация и спасает — меняем XML и все. А вообще примеров много, если долго пользовать, то потом не сможешь без этого жить.

                    Насчет Unit-тестирования — допустим объект А зависит от В, а от — от С. С создается в В без моего ведома. А теперь вопрос — как я могу подменить тот объект, который создается без моего участия вообще? Правильно, я не могу — если только не начну вообще все цепочки создавать вручную, что просто нереально. С контейнером же, я могу подменить реализацию объекта, независимо от того, на какой он «глубине» находится.

                    Тут я имел ввиду что если, например, для использования какого-то сервиса его нужно настраивать (к пр. передавать гору данных в конструктор), то легче это действие централизовать чтобы потом можно было эту конфигурацию менять централизованно. В тех же unit-тестах, например.
                      +3
                      Незнаю как в версии для .NET, но в версии для Java Spring уже давно поддерживает конфигурацию аннотациями.
                        0
                        Можно конфигурировать содержимое IoC контейнера Spring.NET и в коде и даже в стиле Fluent.
                      +3
                      Unity предоставляет возможность конфигурирования без XML файлов. Вся конфигурация выполняется средствами языка, что гораздо удобнее и надежнее. Тоже самое можно сказать про фреймворки Autofac и Ninject — последний мой любимый, им я и пользуюсь. Если кому то это интересно, у меня есть PowerPoint презентация про Ninject — могу выложить.

                      DI столь же уместны как и фабрики. В случае с DI код получается «прозрачнее» — обычно нет необходимости явного требования объекта, создание необходимых экземпляров проходит за сценой. К тому же, проще сконфигурировать одно правило, чем добавить фабрику.

                      Переопределение свойств в ручную — это хорошо, но когда у тебя сотня классов, создавать связи в ручную не очень интересно.

                      Цель DI — избежать самокофигурирующихся объектов, т.к. это снижает возможность их повторного использования и тестирования.
                        0
                        На презентацию бы глянул :)
                          0
                          Я подумал… Я ее переделаю, расширю и выложу как несколько статей. В том числе и про использование Ninject совмесно с ASP.NET MVC.
                            0
                            Кстати да, как раз две темы которые, как мне кажется, стоит осветить (хотя в других блогах они есть) — это поддержка asp.net mvc и wpf (хотя постойте, у autofac с mef тоже интеграция есть что ли?). Другое дело что поскольку эта инфа уже есть где-то еще, не уверен что ее стоит пеперисывать. Может и стоит, ради того чтобы она была понятней.
                              0
                              Есть, и с MEF и с MVC и просто c ASP.NET
                        0
                        Спринг плох XML-настройкой, в большом приложении это просто лишняя работа как при разработке так и при поддержке.

                        Если в бизнес приложении никогда нет необходимости в автоматическом IService[], то это не слишком удачная архитектура.
                        Фабрики надо писать и поддерживать, хороший контейнер — не надо (XML ни разу не использовал, зачем).

                        В юнит тестировании очень помогает automocking container, но это не столь важно. Важнее то, что когда классы спроектированны так, чтобы все вспомогательные объекты можно было переопределить через свойства или конструктор, то чтобы создать их в самом приложении можно:
                        a) либо написать много кода, который ссылается на все библиотеки приложения и требует поддержки
                        б) использовать IoC

                        Я обычно выбираю пункт б).
                        +1
                        Я не согласен с Вами насчет юнит тестирования.

                        Одна из идей IOC в том, чтобы собрать независимые модули системы вместе. А идея юнит тестирования чтобы эти независимые модули протестировать. Так что IOC и юнит тестирование тесно связаны и всегда используются вместе.
                          +1
                          у нас в огромном проекте используется spring именно для IoC.
                          позволяет переконфигурировать приложение без перекомпиляции. Модули (jar'ки) добавляются, удалятся, заменяются, переопределяя друг друга — в итоге получаем нужную конфигурацию для конкретного деплоймента.
                            +2
                            >Ни одного реального, а главное уместного примера применения IoC контейнеров в бизнес приложениях пока не встречал…

                            Давайте я дам конкретный пример.
                            У меня есть модуль–блог. Данные храню на своем сервере. Пришел заказчик, сказал, что у него уже есть блог на Google Blogger'e. Поскольку у меня был легкий сервис, отделенный от всей логики, который только и делал, что писал и читал в/из базы, я заменил его другим, который стал писать и читать в/из сервисов Google Blogger.

                            Применив Unity, я заменил только тонкий сервис, не трогав ничего остального и не переписывая тесты главной логики.

                            Если завтра придет другой чувак и попросит использовать другой движок – сделать это будет легко, и самое главное – у меня не будет болеть голова, что я что–то сломаю. Тесты проходят на главную логику, отдельные на сервис – значит все в порядке.
                              +1
                              >Вопрос такой, в какой системе IoC контейнер может быть полезен при разработке бизнес приложений и на бою?

                              самое простое решение — архитектура черных ящиков, или «черных слоев», когда при пуске приложения контейнер проходит по всем слоям и регистрирует задаваемые слоями реализации. При этом в сборке реализацию можно скрыть от лишних глаз, т.е. добиться полной инкапсуляции в слое. А это очень круто.

                              >Чем IoC лучше?
                              Да ничем он не лучше. Это просто паттерн автоматического определения зависимостей. Он есть в контейнере Unity. Контейнер Юнити описывается автором только потому, что это типа «модное и современное» решение, хотя, судя по ответам на технические вопросы, автор только-только что-то прочитал. IoC можно вообще убрать, написав свой контейнер или взяв простой существующий. System.ComponentModel.Design.ServiceContainer, например (хотя, если вы джавист, то это вам ничего не скажет).

                              Фишка удобства тестирования не выходит из концепции Unity, а выходит из концепции построения классов, которые удовлетворяют шаблону определения зависимостей. Ты должен или проставить зависимость в конструкторе или в свойстве класса, причем, желательно определив интерфейс. Значит, очень легко настроить mock зависимостей (мок интерфейса очень легко сделать) и протестировать фукнционал класса. Unity контейнер здесь не причем. Он всего лишь вставляет зависимости.

                              +1
                              Вы не могли бы к этому элементарному примеру ещё сделать такой же пример по юнит тестам, про которые упомянули.
                                0
                                Обязательно — возможно в следующем посте.
                                0
                                А вот есть прикольная задачка уже не для начинающих — связать ASP.NET MVC с Unity. Требований в целом три:

                                — обеспечить связывание инфраструктуры контроллеров MVC с инъектором
                                — модульность
                                — уникальность контейнера в разрезе сессии

                                Я бы расписал все, но блин, не успеваю вообще ничего, кроме работы. А вам в рамках обучающих методик будет полезно, имхо.
                                  +3
                                  Да, для WPF тоже есть свои особенности. Было бы неплохо все это покрыть, но я не знаю, нужна ли хабру серия постов про Unity/DI/IoC.
                                    +2
                                    Мне это было бы очень интересно. Думаю я не один такой.
                                      0
                                      Категорически нужна.
                                    0
                                    Для тех кому нравятся скринкасты, и не пугает технический английский
                                    James Kovacs' roll-your-own IoC container. Более доходчивого описания IoC не всречал.
                                      0
                                      Хорошие скринкасты с введением в IoC контейнеры Castle Windsor (наверное, самый сейчас популярный), Ninject и StructureMap. На английском.

                                      dimecasts.net/Casts/ByTag/IoC
                                        0
                                        Около года назад тоже озадачились выбоорм IoC фреймворка. Выбрали Autofac. Кроме различных вкусностей, понравилась его легковесность и простота конфигурации. Всё-таки Unity немного громоздок, посмотрите например на код его материализатора объектов.

                                        P.S. IoC используем для разработки линейки продуктов.
                                          0
                                          Хм. Чем вам не угодил IServiceProvider и IServiceContainer из 2.0? Использование новых версий C# это раз, использование стороннего фреймфорка — два. Когда можно сделать (а в этом случае уже сделано) проще, то зачем платить больше?
                                            0
                                            * сделано — имелась в виду реализация ServiceContainer из System.ComponentModel.Design
                                              0
                                              Если я правильно понимаю, ServiceContainer требует чтобы все объекты были созданы заранее, или явно создавились через callback.
                                              Тогда непонятно в чём бонус.

                                              А разве можно написать хоть немного сложное приложение без использования сторонних фреймворков?
                                                0
                                                Service Provider — это совсем не Dependency Injection.
                                                0
                                                Кстати говоря, есть отличный майкрософтовский фреймворк, использующий паттерны DI и IoC. Называется Smart Client Software Factory (SCSF). Мы его юзаем в нашем проекте. Все довольны. А для Java, как уже было сказано выше существует Spring Framework.
                                                  0
                                                  Предлагаю заглянуть в блог одного моего товарища: gandjustas.blogspot.com . Там есть толковые посты по теме, найти их можно по тегам «UNITY», «IOC-КОНТЕЙНЕР».
                                                    0
                                                    У меня дурацкий вопрос.
                                                    Если абстрагироваться от всяческих фреймворков и .NET… Правильно же, что IoC это такой хитрый контейнер, который может отдать объект по классу. А если он ещё и отслеживает зависимости и удовлетворяет их, то тут ещё и DI.
                                                    Так вот вопрос: если всё это так, то чем IoC-контейнер отличается от фабрики?
                                                      0
                                                      Это по сути и есть фабрика. В spring прямо так и называется BeanFactory. Т.е. BeanFactory и является IOC контейнером.

                                                      BeanFactory factory = new XmlBeanFactory(«beans.xml»);
                                                        0
                                                        Здесь нужно уточнить. Контейнер отличается от фабрики прежде всего, тем, что сохраняет ссылки на создаваемые им объекты и управляет их жизненным циклом. Фабрика же занимается только созданием объектов. пруф1, пруф2

                                                        В spring IoC контейнер создается как объект ApplicationContext.
                                                        The interface org.springframework.context.ApplicationContext represents the Spring IoC container and is responsible for instantiating, configuring, and assembling the aforementioned beans.
                                                        При этом ApplicationContext наследует также от BeanFactory
                                                        public interface BeanFactory
                                                        The root interface for accessing a Spring bean container. This is the basic client view of a bean container; further interfaces such as ListableBeanFactory and ConfigurableBeanFactory are available for specific purposes.
                                                        Т.е. IoC контейнер в Spring является также и фабрикой, но имеет более широкий функционал, чем создание объектов. В официальной документации для создания контейнера рекомендуется использовать ApplicationContext. пруф3
                                                      0
                                                      Паттерн DI — это одна из реализаций принципа IoC

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

                                                      Самое читаемое