Pull to refresh

Использование IoC контейнеров. За и Против

Reading time 4 min
Views 21K
В свете выхода новой версии Enterprise Library, одной из важнейших частей которой является IoC-контейнер Unity, лично я ожидаю всплеск интереса к теме IoC(Inversion of Control) контейнеров, которые, по сути, являются реализацией достаточно известного паттерна Service Locator.

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

Предыстория проблемы


Обычно к знакомству с IoC-контейнерами подталкивает осознание необходимости следования принципам проектирования классов (SOLID), а именно, последний принцип, который говорит о том, что каждый класс должен зависеть не от конкретных объектов, а от абстракций(интерфейсов), и совсем замечательно, когда все эти зависимости объявляются в конструкторе.

То есть код вида:
class TextManager {
 public TextManager(ITextReader reader, ITextWriter writer) {
 }
}


* This source code was highlighted with Source Code Highlighter.

это хорошо, а код вида:

class TextManager {
 public TextManager(TextFromFileReader reader) {
 }

 public property DatabaseWriter Writer {
  set {
    _writer = value;
  }
 }
}

* This source code was highlighted with Source Code Highlighter.

это плохо.

При этом, если приложение/библиотека у вас большая, ITextWriter используется во многих классах, а какой именно Writer будет использоваться определяется на входе в библиотеку (допустим, у нас есть Writer'ы в БД и файл с общим интерфейсом), то логично возникает желание как-то связать ITextWriter с DatabaseWriter'ом где-то в одном месте.

До SOLID-ов считалось вполне нормальным объявить внутри библиотеки в статическом классе переменную типа ITextWriter и хранить конкретный используемый на текущий момент Writer там. Но когда начинаешь задумываться о нормальной архитектуре… :) минусы статичных классов становятся очевидными — совершенно непрозрачно от чего именно каждый класс зависит, что ему нужно для работы, а что нет, и страшно даже представить, что «потянется» вместе с этим классом, если вдруг необходимо будет его перенести в другой проект или хотя бы другую библиотеку.

IoC-контейнер


Что же предлагается нам для решения проблемы? Решение давно придумано в виде IoC-контейнеров: мы «связываем» интерфейсы с конкретными классами, как только получаем необходимую информацию:

 var container = new UnityContainer();
 container.RegisterInstance<ITextWriter>(new DatabaseWriter());


* This source code was highlighted with Source Code Highlighter.


и имеем удобный способ создания объектов:
 container.Resolve<TextManager>();
* This source code was highlighted with Source Code Highlighter.


И если на примере одной зависимости, преимущество контейнеров неочевидно, то если представить, что конструкторы требуют 2-3 интерфейса, и таких классов у нас хотя бы 5-10, то о применении контейнеров покажется многим настоящим спасением.

Когда контейнер — хорошо..


Собственно, Unity и создавался для активного применения в сложных составных приложениях, с множеством реализаций идентичных интерфейсов и нелинейной логикой взаимозависимостей между реализациями. Говоря проще, если у нас на всю библиотеку не один-единственный интерфейс IWriter с двумя реализациями DbWriter и TextWriter, а еще, к примеру, IReader и ITextProcessor, для каждого из которых тоже существует 3-4 реализации, и TextWriter работает только с CsvReader'ом и ExcelReader'ом, а какой именно из ридеров надо использовать, зависит от конкретного типа текущего TextProcessor'а, ну и от фазы луны заодно.

Очень сложно привести конкретные примеры кода, применение Unity в котором было бы, с моей точки зрения, обоснованно, и не загромоздить текст тонной ненужного кода. Но описанный «пример» создает некое подобие такой сложной и комплексной задачи :)

А когда — не очень


Применение конструкций типа container.Resolve(); кажется очень удобным, и поначалу так и тянет использовать его почаще, и инстанциировать классы таким образом везде, где это только возможно. Всё это влечет к пробрасыванию контейнера «по всей глубине» библиотеки/приложения, и организации доступа к IUnityContainer'у либо через конструкторы классов, либо, возвращаясь к прошлому, через статичные классы.
class TextManager {
 public TextManager(IUnityContainer container) {
  _writer = container.Resolve<IWriter>();
  _reader = container.Resolve<IReader>();
 }
}

* This source code was highlighted with Source Code Highlighter.


Это и является крайностью использования UnityContainer'а. Потому что:
  • наличие в конструкторе «непонятного» IUnityContainer скрывает пути его использования внутри класса, и постороннему человеку при повторном использовании вашего класса будет неясно, какие именно объекты/интерфейсы требуются классу для работы;
  • это усложняет Unit-тестирование, опять же потому, что непонятно, какие интерфейсы и операции замещать;
  • и, наконец, это прямое нарушение принципа Interface Segregation, говорящего об использовании интерфейсов без «лишних» методов (методов, не используемых в классе).


...Profit!


Подытоживая, мне кажется, что использование IoC-контейнеров идеально именно в ситуациях со сложными переплетениями взаимозависимостей между конкретными реализациями, и только на самых верхних уровнях библиотеки/приложения.
То есть сразу после точки входа в библиотеку следует регистрировать в Юнити реализации интерфейсов, и как можно раньше резолвить необходимые нам типы. Таким образом мы пользуемся всей мощью Юнити по упрощению процесса генерации сложнозависимых объектов, и в то же время следуем принципам хорошего дизайна и сводим к минимуму использование весьма абстрактного «контейнера» в нашем приложении, тем самым упрощая его тестирование.

P.S. Поскольку сам в данной теме далеко не гуру, в комментариях буду рад услышать о собственных ошибках, а также о других возможных применениях IoC-контейнеров.
Tags:
Hubs:
+15
Comments 80
Comments Comments 80

Articles