Часть 5. Bun Framework :: Dependency Injection Container
2014-06-05 в 20:08 Bun Framework Dependency Injection PHP
Мы уже создали достаточно функциональых компонетов фреймворка, чтобы задуматься о том, как мы будем их инициализировать, использовать и управлять зависимостями между ними. В предыдущих статьях был разработан компонент конфигурации фреймворка, набор компонентов Http: Request & Router. Класс роутер у нас получился зависим от конфигурации и класса запроса. В этой статье я раберу на примере Bun Framework. Создание и тестирование небольшого но полноценного сервиса, который будет управлять зависимостями в PHP-приложении.
1. Container Config
Для начала определимся с архитектурой нашего будущего контрейнера зависимостей. Я буду использовать уже имеющийся у нас механизм конфигураций. Управление зависимостями и инциалиация компонетов фреймворка и приложений будет осуществляться с стиле ServiceContainer. Т.е. каждый класс, выражающий собой отдельный компонент системы, будет представляться в виде сервиса. Для упрощения механизма управления зависимостями будет использоваться механизм имплементации aware-интерфейсов сервисов. Т.е. если мы хотим получить в своем сервисе сервис Request, то нам нужно будет реализовать интерфейс RequestAwareInterface. Такой подход делаем код, основанный на использовании контейнера зависимостей более читаемым и понятным – при просмотре кода компонента сразу очевидны его зависимости. Также достаточно удобно искать в проекте сервисы, которые зависят от того или иного компонента.
Также в конфигурации сервиса будут предусмотрены зависимости от настроечных параметров или просто от константных значений. Т.е. в ContainerConfig мы сможем прописать 'aware' => array('setParameter' => ':config.param')
и при инициализации сервиса будет вызван его метод setParameter, с передыннм ему аргументом в виде значений из конфигурации "config.param". Таким же образом можно будет и указывать зависимости от другого сервиса, используя немного другой синтаксис: 'aware' => array('setService' => '@bun.service')
. А теперь привожу конфигурацию контейнера, которая у меня получилась на момент написания статьи:
2. Bun\Core\Container\Container
Теперь переходим к самому компоненту контейнера. Сразу отмечу, что для контейнера я буду использовать паттерн Singleton. Но при этом оставлю возможность переопределить контрейнер и использовать его без ограничений синглтона. Singleton оправдан в данном случае, т.к. это разумное ограничение: использовать только один инстанс сервис-контейнера в приложении. Есть и свой недостаток – мы оставляем возможность получить рабочий инстанс контейнера в любом месте в приложении. Но с другой стороны это может быть и удобством. Логика работы контейнера проста и код получился довольно простым и лаконичным. У нас будет один публичный метод: get(), который в качестве аргумента принимает имя сервиса, указанное в конфигах. Кстати, тут обращу внимание, что я добавил возможность задавать alias'ы для сервисов. Иногда это очень удобно, т.к. во фреймворке я выбрал общую логику построения имен сервисов:
- имя состоит из пространства имен, разделители "\" в котором заменяются на ".". При этом имя класса и неймспейса преобразуются из camelCase в under_score.
- Если имя класса содержит часть пространства имент, то из названия сервиса можно это часть исключить. Как пример: для класса
Bun\Core\Container\Container
имя сервиса будет"bun.core.container"
Но вернемся к логике. Она заключается в том, что мы ищем в настройках контейнера имя переданного нам сервиса, создаем объект нужного класса, затем ищем его зависимости и создаем для них при необходимости такие же объекты. Далее привязываем созданные зависимости к запрашиваемому сервису. Потом сохраняем обхект в кэш уже инициализированных сервисов – далее при повторном запросе сервиса возвращать будем сразу оттуда. Инициализированные при этом зависимости тоже сохраняются при этом в кэше. Также при этом мы заботимся о том, чтобы у класса не было рекурсивной зависимости от самого себя, иначе наше приложении уйдет в бесконечную рекурсию. Плюс при всем при этом, нужно еще сделать, чтобы сервисы были доступны через свои алиасы и не инициализировались лишний раз при вызове сервиса через свой алиас. На это вроде все, показываю код:
Bun\Core\Tests\Container
А теперь затестим что у нас вышло. Честно говоря, хоть логики и проста, но я удивился, когда мой контейнер запустился и сработал с первого раза в тестах.
Тестить будем с помощью тестовых сервисов, которые вы могли заметить в конфигурации контейнера выше. Вот их простенький код:
Также приведу код aware-инетрфейса для тестовых сервисов:
Тут все просто, понятно я думаю. Далее пишем сам тест. В тесте мы проверим, что контейнер:
- Правильно выдает нам нужный класс
- Правильно при этом добавляет ему зависимости
- И что при повторном вызове сервиса, мы получаем уже сохраненную копию объекта
Вот короткий тест кейс:
На этом все, запускаем тест, проверяем что все работает.
В завершении скажу, что Dependency Injection на вид кажется очень сложным паттерном. Но если детально разобраться, написать свой велосипед – понимаешь его суть и прелесть. В будущем мы еще не раз убедимся, что DI Container нам здорово помощет при написании модульных тестов.
Автор Yakov Akulov
Комментарии (0) написать
Написать комментарий: