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

//Написано в далеком апреле 2010-го года. Изначально опубликовано на хабре. В контексте следующей записи будет интересно вспомнить «как оно было год назад» и что изменилось с тех пор :)

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

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

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


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

То есть код вида:

class TextManager {
    public TextManager(ITextReader reader, ITextWriter writer) {
    }
}

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

class TextManager {
    public TextManager(TextFromFileReader reader) {
    }

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

это плохо.

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

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

IoC-контейнер

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

  var container = new UnityContainer();
  container.RegisterInstance(new DatabaseWriter());

и имеем удобный способ создания объектов:

  container.Resolve();

И если на примере одной зависимости, преимущество контейнеров неочевидно, то если представить, что конструкторы требуют 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();
       _reader = container.Resolve();
    }
}

Это и является крайностью использования UnityContainer’а. Потому что:

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

…Profit!

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

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

Опубликовать в Facebook
Опубликовать в Google Plus

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *