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

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

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

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

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

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

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

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

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

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

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

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

IoC контейнеры Link to heading

Возможно вы уже задались вопросом, если системе нужен граф объектов, то может стоит описывать конфигурацию в виде графа объектов? Тогда можно было бы один раз написать транслятор превращающий конфигурацию в тот самый 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 контейнер это, по моему мнению, один из таких инструментов.

Стоит ли? Link to heading

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

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