21 декабря, 2009

IoC контейнеры

Если вы пишете на объектно-ориентированном языке, то вы должны быть знакомы с "джентльменским набором" принципов, которые очень полезны при написании кода. К таким принципам относятся: single responsibility principle, open-closed principle, interface segregation principle и другие. Общий эффект их использования заключается в том, что количество сущностей (классов и интерфейсов) в системе стремительно растет. В этом есть как положительные, так и отрицательные стороны.

Один из положительных моментов заключается в том, что у каждой сущности появляется строго выделенная роль, для работы которой требуется меньше кода. Такую роль проще протестировать, ее поведение проще понять, а следственно и изменить.

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

Два представления конфигурации

В правильно построенной объектно-ориентированной системе нет конфигурации. Довольно революционное заявление, верно? Суть в том, что с точки зрения самого кода несущего полезную нагрузку конфигурации не должно существовать. Одни сущности требуют в качестве зависимостей другие (опираясь на интерфейс). Где, кто и как будет инициализировать зависимости, — вопрос не имеющий к пользовательскому коду никакого отношения.

Я репозиторий пользователей, дайте мне в конструктор подключение к базе данных, и я смогу искать для вас пользователей по идентификатору и логину.

Именно так должны вести себя все сущности. Другими словами, они не должны сами заниматься поиском и разрешением своих зависимостей.

Можно сказать иначе. С точки зрения самой системы во время выполнения, ее конфигурация задается графом объектов. У нас есть граф объектов по которому туда сюда бегают сообщения. Конечное поведение системы зависит от того как именно сконфигурирован этот граф.

Это очень удобное описание системы с точки зрения компьютера, но не с точки зрения человека. Человеку нужен какой-то текстовый файл, в котором можно было бы подправить настройки подключения к БД, цвет шрифта, timeout подключения к внешнему сервису и т.д.

Давайте остановимся на минутку и подумаем что это значит. Мы имеем два способа которые описывают поведение системы: граф объектов и конфигурационные файлы.

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

Это вам ничего не напоминает? Object-relational impedance, но только в области конфигурирования.

IoC контейнеры

Возможно вы уже задались вопросом, если системе нужен граф объектов, то может стоит описывать конфигурацию в виде графа объектов? Тогда можно было бы один раз написать транслятор превращающий конфигурацию в тот самый runtime граф, который необходим для работы системы.

Такими трансляторами и являются IoC контейнеры.

Здесь я рассматриваю только те IoC контейнеры, которые позволяют описывать конфигурацию отдельно от самого кода. Библиотеки, называющиеся IoC контейнерами, и не выполняющие вышеуказанное требование, таковыми не являются, так как не выполняют самой главной задачи IoC контейнера, — изоляции production кода от механизма локализации и удовлетворения зависимостей.

У себя в проекте мы написали свой IoC контейнер. Фактически это порт Spring beans на PHP. Согласен, звучит странно. Но spring beans, по моему мнению, идеологически верен, так как он не требует менять код для своего использования.

Конфигурация в нашем случае описывается примерно следующим образом:

<?xml version="1.0" encoding="utf8"?>
<beans xmlns="http://farpost.com/slr/injector">
  <bean id="masterConnection" class="MySqlConection">
    <property name="host" value="hostname" />
    <property name="user" value="john" />
    <property name="password" value="secret" />
    <property name="db" value="orders" />
  </bean>
    
  <bean id="userRepository" class="SqlUserRepository">
    <constructor-arg ref="masterConnection" />
  </bean>
</beans>

Фактически, это эквивалентно следующему коду:

$masterConnection = new MySqlConnection();
$masterConnection->setHost("hostname");
$masterConnection->setUser("john");
$masterConnection->setPassword("secret");
$masterConnection->setDb("orders");

$userRepository = new SqlUserRepository($masterConnection);

Многословно? Да, наверное. Но это дешевле чем каждый раз писать код создающий объекты на основании конфигурации, когда у вас сотни таких объектов.

Существует несколько способов, как можно сделать вышеприведенный XML гораздо более лаконичным. Некоторые из них мы уже опробовали и они работают вполне хорошо, некоторые мы только хотим опробовать. Здесь я не буду углубляться в детали. Если вам интерестно, дайте мне знать, быть может это стоит отдельной заметки.

Использование IoC контейнеров дает следующие выгоды:

  • единый формат описания конфигурации вне зависимости от подсистемы;
  • автоматический lazy для всех объектов;
  • декларативное управление зависимостями, — не требуется менять код для изменения поведения системы;
  • гибкое управление зависимостями, — в отличии от singleton и toolkit/registry разным клиентам можно подпихивать разные имплементации.

Последний пункт имеет далекоидущие последствия. Очень часто бывает что разным клиентам нужны разные имплементации одного и того же интерфейса. Например, в большинстве случаев клиенты используют различного рода data provider'ы обернутые в кеширующие декораторы. Тем не менее, некоторые клиенты в силу специфики задач могут генерировать большое количество cache miss'ов. Становится разумным просто отключить кеш для этих клиентов. Если ссылка на data provider хранится в singleton'е или реестре (registry), то подменить ее для конкретных клиентов становится просто невозможно.

Конечно, теперь конфигурация становится сложнее. Но в любой сложной системе конфигурация будет самой сложной ее частью. Я думаю смело можно сказать, что сложность большинства систем определяется сложностью их конфигурации. От этой сложности никуда не деться. Но мы можем обеспечить себя инструментами позволяющими справляться с этой сложностью. IoC контейнер это, по моему мнению, один из таких инструментов.

Стоит ли?

Решать вам. Идея применения IoC контейнера в скриптовых языках, может показаться абсурдом. Тем не менее, существует рубикон перевалив через который становится понятно, чтос точки зрения сложности эксплуатации разницы между скриптовыми языкам и всеми остальными не существует.

Поэтому ответ на вопрос "стоит ли?" заключается в вопросе "перевалили ли вы через этот рубикон?".


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

  1. А вас "декларативное управление зависимостями" не пугает? Например, как будет выглядеть включение отладочных врапперов для клиента с определенной кукой? Какой-то специальный механизм?

    ОтветитьУдалить
  2. Я ни в коем случае не являюсь ярым фанатом декларативности :) Сделать систему полностью декларативной можно, но это экономически не выгодно, — определенная доля императивности, я считаю, очень полезна.

    Приведенный вами пример мы разруливаем при помощи post processor'ов. Это своего handler, который может дополнительно обработать (изменить, подменить, обернуть) уже созданный и инициализированный объект перед его возвратом клиенту. Через этот механизм у нас например реализовано разименование переменных. Например, можно написать , а во время выполнения переменная username разименуется. Во что и как, это тоже вопрос реализации.

    Однако, этих post processor'ов у нас по пальцам пересчитать. Подразумевается, что программистам не приходится свои имплементации. Если это не так, то ценность идеи под вопросом.

    ОтветитьУдалить
  3. Спасибо.Попробую придумать ситуацию, когда полная императивность (lmbToolkit) проигрывает декларативному конфигурированию.

    ОтветитьУдалить
  4. Собственно мы сейчас находимся как раз в той ситуации когда большинство зависимостей получается через toolkit. Так что наше движение в сторону IoC происходит уже после того как мы опробовали другие более традиционные решения.

    Основными предпосылками для нас были: необходимости везде руками писать lazy, зависимость production кода от toolkit'а (тяжело тестировать, менее изолированные компоненты) и невозможность дать разным клиентам разные имплементации одного и того же интерфейса (последнее нас очень ограничивает).

    ОтветитьУдалить
  5. 1. Тут я бы, наверное, воспользовался магией. Ловил бы getLazySomething() и кэшировал в себе вызов getSomething()
    2. Не очень понял, чем тут поможет IoC-контейнер. Он же не уменьшит количество зависимостей. Все равно их надо сетапить в фикстуре.
    3. У нас видимо все таки разные по возможностям тулкиты: $statDbConnection = Toolkit::instanse()->getConnection(Stat::CONNECTION);

    ОтветитьУдалить
  6. С первым пунктом согласен. Как я и говорил, проблема lazy решаема в пределах toolkit'а.

    По второму пункту. Toolkit добавляет сложность при тестировании. Так как все компоненты ищут свои зависимости через toolkit, то провести тестирование без toolkit'а невозможно. Более того не получится deploy'ить компоненты в среду где нет toolkit'а. Например наши библиотеки используют другие отделения внутри нашей компании. И сейчас нам приходится навязывать им использование toolkit'а, что не очень хорошо.

    По третьему пункту. Это совсем не решение (ну по крайней мере в нашем случае), потому что для этого надо менять production код. То есть для того чтобы клиенту подсунуть другую имплементацию интерфейса, надо найти его (клиента) в кодовой базе и поменять тот самый аргумент передаваемый в toolkit.

    ОтветитьУдалить
  7. Прошу прощения, я неточно ответил на второй ваш вопрос. Поэтому хочу внести некоторые уточнения.

    Все верно IoC контейнер никоим образом не уменьшает количество связей между сущностями. Но когда вы пишете фикстуру теста гораздо проще все зависимости передавать в сам тестируемый класс, чем в toolkit. Это более явно, чем инициализация глобального реестра объектов. И, к тому же, в случае использования toolkit'а приходится следить, чтобы фикстура одного теста не повлияла не фикстуру другого, так как toolkit глобален.

    Вообще toolkit — это такой же самый global state, как singleton. Он только немножко более управляем :)

    ОтветитьУдалить
  8. toolkit, IoC и прочие "непрямые" DI, ИМХО, стоит применять только в высокоуровневых местах, типа приложений. А в библиотеках ограничится явной передачей.

    Поэтому и проблем с повышением прозрачности ящика не возникает. Если мне надо что-то протестировать в приложении, то я делаю хэлпер или сервис и единственная там привязка к тулкиту будет в получении коннфигурации. Можно и от него избавится, но привык как-то.

    С другой стороны - введение IoC и эту проблему не решит. Вместо зависимости от toolkit'а другие отделы получат зависимость от IoC.

    Плюс IoC тут скорее в том, что сама его природа не даст сделать зависимость внутри класса, как в случае tookit.

    ОтветитьУдалить
  9. Хехе, мы уже почти пять лет используем такое решение, пока кейзов, которые бы упирались в него найдено было единицы:

    http://git.shadanakar.org/?p=onPHP.git;a=blob;f=meta/dtd/meta.dtd;h=35a0b81a57a08f74ef9f8d00e0a98947f8ed8214;hb=HEAD

    ОтветитьУдалить
  10. Хм... Я так понял, что в вашем случае XML используется в основном для автогенерации кода, а не для dependency management.

    ОтветитьУдалить
  11. а код Вашего контейнера не дадите? ))

    ОтветитьУдалить
  12. Мы думаем над этим. Возможно, заопенсурсим если будут ресурсы.

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