Тема DI/IoC достаточно простая, но в сети очень сложно найти хорошее описание того, как это работает и зачем это нужно. Вот моя попытка, с использованием Unity. Хорошо ли объяснена тема – судить вам.
Давайте создадим простенький пример:
// сервис. содержит бизнес-логику
public class MyService
{
public MyService()
{
⋮
}
public void DoSomething()
{
⋮
}
}
// окошко. пользуется бизнес-логикой
public class MyWindow : Form
{
private MyService service;
public MyWindow()
{
service = new MyService();
}
}
// програмка которая показывает окошко
public class MyProgram
{
static void Main()
{
⋮
/// создаем окно
Application.Run(new MyWindow);
}
}
В этом коде одна проблема: класс
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
{
static void Main()
{
⋮
var uc = new UnityContainer();
Application.Run(uc.Resolve<MyWindow>());
}
}
Методом
Resolve() контейнера, мы запрашиваем не только создание объекта типа
MyWindow, но и автоматической создание
всех его зависимостей. Теперь посмотрим на то, как можно получить создание сервиса (т.е. зависимой части) автоматически. Для начала, вытащим интерфейс сервиса, чтобы его можно было в последствии поменять:
interface IService
{
void DoSomething();
}
public class MyService : IService
{
⋮ // все как и раньше
}
Теперь меняем
MyWindow чтобы использовался именно интерфейс. Есть несколько вариантов того, как можно добавить ссылку на сервис чтобы контейнер его инициализировал. Вот один из них:
public class MyWindow : Form
{
private IService service;
public MyWindow(IService service)
{
this.service = service;
}
}
Теперь осталось сделать только одно – сказать контейнеру чтобы по запросу объектов типа
IService он выдавал
MyService:
public class MyProgram
{
static void Main()
{
⋮
var uc = new UnityContainer();
uc.RegisterType<IService, MyService>();
Application.Run(uc.Resolve<MyWindow>());
}
}
Вот и все!!! Теперь при запуске программы переменная
service окошка будет инициализирована
автоматически. Это называется «инъекция в конструктор» (constructor injection). Хотите оставить конструктор пустым? Пожалуйста:
public class MyWindow : Form
{
[Dependency]
public IService Service { get; set; }
public MyWindow()
{
⋮
}
}
Сменив поле на свойство и пометив его аттрибутом
[Dependency], мы намекнули контейнеру, что его нужно инициализировать при создании класса. Результат тот же, что и с конструктором. Такой прием называется «инъекция в свойство» (setter injection).
Вот и все! Спасибо за внимание! Если вас заинтересовала тема и вы живете в Петербурге, приходите к нам завтра
на встречу.
Читать 2ю часть серии
комментарии (44)
Другими словами, если мне необходимо иметь 2 объекта с интерфейсом IService в одном MyWindow, то фреймворк позволит это сделать?
Только не забудьте, что by default, контейнер выдает синглтоны. Чтобы получить разные копии сервисов, нужно слегка изменить регистрацию:
public class MyFirstServiceImpl : IService{}
public class MySecondServiceImpl : IService{}
По логике, если они имплементируют один и тот же интерфейс, то и использование у них будет однородное.
В таком случае параметр можно заменить на IService[] services и дальше работать со всеми имплементациями сразу.
Несколько лет назад я прочитал ту самую статью Фаулера и загорелся. И стал я использовать Spring Framework. Отличная, надо сказать, библиотечка. Представляет очень функциональность по гибкую xml-настройке DI.
Чем данный IoC контейнер (Unity) лучше хотя бы того же Spring?
Про бой и бизнес приложения
Ни одного реального, а главное уместного примера применения IoC контейнеров в бизнес приложениях пока не встречал, обычно их используют ради технологического фетишизма или из особой любви к Фаулеру.
Все места, где использовался IoC, в моем рабочем проекте были безжалостно заменены на обычные фабрики т.к. стало ясно, что без перекомпилляции никогда нет нужды подменять реализацию, а дополнительная, не нужная в боевых условиях, xml-конфигурация только отягощяет решение.
Вопрос такой, в какой системе IoC контейнер может быть полезен при разработке бизнес приложений и на бою?
Даже так, в каких реальных приложениях его использование может быть уместно?
Про unit тестирование
Если хочешь unit-тестировать код, спроектируй его так, чтобы IoC был не нужен, а все вспомогательные объекты можно было переопределить через свойства или конструктор.
Чем IoC лучше?
Про конфигурацию
«Если наш сервис требует настройки, его придется настраивать в каждом классе, который его использует.»
Что имел ввиду автор? Передавать константные параметры в конструктор? Чем тут IoC контейнер лучше фабрики и грамотно спроектированной структуры конфигурационного файла или даже объекта, который конфигурирует себя сам?
Примеров актуального использования контейнеров в сети — миллион. Один из примеров — когда при неправильно работающем приложении нужно заменить обычную реализацию на динамический прокси с детальным логгированием. Без перекомпиляции. На живой системе. Тут-то внешняя конфигурация и спасает — меняем XML и все. А вообще примеров много, если долго пользовать, то потом не сможешь без этого жить.
Насчет Unit-тестирования — допустим объект А зависит от В, а от — от С. С создается в В без моего ведома. А теперь вопрос — как я могу подменить тот объект, который создается без моего участия вообще? Правильно, я не могу — если только не начну вообще все цепочки создавать вручную, что просто нереально. С контейнером же, я могу подменить реализацию объекта, независимо от того, на какой он «глубине» находится.
Тут я имел ввиду что если, например, для использования какого-то сервиса его нужно настраивать (к пр. передавать гору данных в конструктор), то легче это действие централизовать чтобы потом можно было эту конфигурацию менять централизованно. В тех же unit-тестах, например.
DI столь же уместны как и фабрики. В случае с DI код получается «прозрачнее» — обычно нет необходимости явного требования объекта, создание необходимых экземпляров проходит за сценой. К тому же, проще сконфигурировать одно правило, чем добавить фабрику.
Переопределение свойств в ручную — это хорошо, но когда у тебя сотня классов, создавать связи в ручную не очень интересно.
Цель DI — избежать самокофигурирующихся объектов, т.к. это снижает возможность их повторного использования и тестирования.
Если в бизнес приложении никогда нет необходимости в автоматическом IService[], то это не слишком удачная архитектура.
Фабрики надо писать и поддерживать, хороший контейнер — не надо (XML ни разу не использовал, зачем).
В юнит тестировании очень помогает automocking container, но это не столь важно. Важнее то, что когда классы спроектированны так, чтобы все вспомогательные объекты можно было переопределить через свойства или конструктор, то чтобы создать их в самом приложении можно:
a) либо написать много кода, который ссылается на все библиотеки приложения и требует поддержки
б) использовать IoC
Я обычно выбираю пункт б).
Одна из идей IOC в том, чтобы собрать независимые модули системы вместе. А идея юнит тестирования чтобы эти независимые модули протестировать. Так что IOC и юнит тестирование тесно связаны и всегда используются вместе.
позволяет переконфигурировать приложение без перекомпиляции. Модули (jar'ки) добавляются, удалятся, заменяются, переопределяя друг друга — в итоге получаем нужную конфигурацию для конкретного деплоймента.
Давайте я дам конкретный пример.
У меня есть модуль–блог. Данные храню на своем сервере. Пришел заказчик, сказал, что у него уже есть блог на Google Blogger'e. Поскольку у меня был легкий сервис, отделенный от всей логики, который только и делал, что писал и читал в/из базы, я заменил его другим, который стал писать и читать в/из сервисов Google Blogger.
Применив Unity, я заменил только тонкий сервис, не трогав ничего остального и не переписывая тесты главной логики.
Если завтра придет другой чувак и попросит использовать другой движок – сделать это будет легко, и самое главное – у меня не будет болеть голова, что я что–то сломаю. Тесты проходят на главную логику, отдельные на сервис – значит все в порядке.
самое простое решение — архитектура черных ящиков, или «черных слоев», когда при пуске приложения контейнер проходит по всем слоям и регистрирует задаваемые слоями реализации. При этом в сборке реализацию можно скрыть от лишних глаз, т.е. добиться полной инкапсуляции в слое. А это очень круто.
>Чем IoC лучше?
Да ничем он не лучше. Это просто паттерн автоматического определения зависимостей. Он есть в контейнере Unity. Контейнер Юнити описывается автором только потому, что это типа «модное и современное» решение, хотя, судя по ответам на технические вопросы, автор только-только что-то прочитал. IoC можно вообще убрать, написав свой контейнер или взяв простой существующий. System.ComponentModel.Design.ServiceContainer, например (хотя, если вы джавист, то это вам ничего не скажет).
Фишка удобства тестирования не выходит из концепции Unity, а выходит из концепции построения классов, которые удовлетворяют шаблону определения зависимостей. Ты должен или проставить зависимость в конструкторе или в свойстве класса, причем, желательно определив интерфейс. Значит, очень легко настроить mock зависимостей (мок интерфейса очень легко сделать) и протестировать фукнционал класса. Unity контейнер здесь не причем. Он всего лишь вставляет зависимости.
— обеспечить связывание инфраструктуры контроллеров MVC с инъектором
— модульность
— уникальность контейнера в разрезе сессии
Я бы расписал все, но блин, не успеваю вообще ничего, кроме работы. А вам в рамках обучающих методик будет полезно, имхо.
James Kovacs' roll-your-own IoC container. Более доходчивого описания IoC не всречал.
dimecasts.net/Casts/ByTag/IoC
P.S. IoC используем для разработки линейки продуктов.
Тогда непонятно в чём бонус.
А разве можно написать хоть немного сложное приложение без использования сторонних фреймворков?
Если абстрагироваться от всяческих фреймворков и .NET… Правильно же, что IoC это такой хитрый контейнер, который может отдать объект по классу. А если он ещё и отслеживает зависимости и удовлетворяет их, то тут ещё и DI.
Так вот вопрос: если всё это так, то чем IoC-контейнер отличается от фабрики?
BeanFactory factory = new XmlBeanFactory(«beans.xml»);