В этой статье предлагаю разобраться чем занимается IoC-контейнер и как это определяет его внутреннюю структуру. Исходить будем из задачи, которую решает контейнер: создать и скомпоновать объекты таким образом, чтобы все зависимости этих объектов были удовлетворены.
Очевидно, что зависимости необходимо некоторым образом описать: объект, называемый так-то, скажем A, зависит от других объектов, например B и C. При этом контейнер ничего не знает о том, какой из объектов мы называем A, а какие — B и C. Это тоже необходимо описать. Контейнер оперирует только теми компонентами, которые он знает (часто их называют бинами). Для создания объекта контейнеру необходимо также знать к какому классу объектов он принадлежит и как долго его помнить. Самый простой способ предоставить все эти данные (с точки зрения устройства контейнера, конечно) — напрямую обратиться к регистрационному методу контейнера, передав ему в качестве параметра некоторый объект с метаданными. Второй вариант: промаркировать наш код некоторым образом, понятным контейнеру. (Стоит помнить, что это приводит к зависимости нашего кода от используемого контейнера.) В этом случае у контейнера должен быть сканер, который найдет весь маркированный код и составит из маркировки и кода необходимое описание. Пользователю контейнера может быть удобно задать метаданные в некотором внешнем источнике и тогда контейнеру необходим компонент, который сможет прочитать метаданные из этого источника. Итого, контейнеру необходим транслятор, который примет от пользователя описание компонентов и их зависимостей и переведёт его во внутреннее представление контейнера.
Компоновка производится путём внедрения одних объектов в другие. Внедрение может происходить как во время создания объекта (зависимости передаются в конструктор) так и после него (зависимости передаются в сеттеры или другие сконфигурированные методы). Внедряться может как один объект так и некоторое их множество (например, все объекты, реализующие определённый интерфейс). Зависимости могут быть обязательными и необязательными. Вариантов внедрения не так уж много и их удобно описать набором стратегий разрешения и внедрения зависимостей.
Имея описание компонентов, их зависимостей и стратегии их внедрения можно переходить к собственно созданию объектов, для чего контейнеру понадобится фабрика. В большинстве случаев создание объекта сводится к вызову его конструктора и сеттеров, в которые передаются ранее созданные зависимости. Дополнительно может вызываться сконфигурированный метод инициализации. Иногда возникает необходимость сконструировать объект определённым способом (например, используя шаблон “строитель”). В этом случае пользователь описывает / регистрирует в контейнере свою фабрику, которой основная фабрика контейнера будет делегировать создание объекта в описанных случаях.
В зависимости от указанного в метаданных времени жизни объект после создания помещается в реестр объектов с таким же временем жизни (реестр - условное название, в коде конкретной реализации ориентируйтесь на scope). Контейнер обращается за объектом сначала в соответствующий реестр, а потом, если нужный объект отсутствует, к фабрике. Объекты находятся в реестре до его закрытия / уничтожения. Контейнер может предоставлять возможность пользователю задать метод объекта, который вызовется перед тем как объект будет забыт.
Надеюсь, эта статья демистифицирует IoC-контейнер в ваших глазах. Я намеренно не привязывался к какой-либо конкретной реализации контейнера и предполагаю, что в деталях вы сможете разобраться сами. В учебных целях могу порекомендовать посмотреть на IoC-контейнер Petite, который входит в состав легковесного фреймворка Jodd. Он достаточно функциональный, но проще того, который предоставляется Spring фреймворком.
Комментарии