Phemto и Паттерн Dependency Injection. Часть 1
Я не встречал хорошего описания паттерна Dependency Injection применительно к PHP.
Недавно ребята из Symfony выпустили свой контейнер DI, снабдив его подробной и хорошей книжкой о том как работать с этим паттерном.
Я вспомнил еще об одной библиотеке для DI, Phemto. Ее автор, — Маркус Бэйкер, создатель SimpleTest. К сожалению на сайте содержится краткая и невнятная справка. тем не менее, проект развиавется, а внутри дистрибутива лежит статья с крайне хорошим объяснением про DI, ну и руководством конечно. Phemto, — очень миниатюрный проект, состоящий из трех не очень больших файлов.
Мне показалось, полезным перевести статью на русский язык и выложить сюда. Статья не очень большая, но содержательная. Ссылку на оригинал дать не могу, оригинал внутри дистрибутива 🙂
На программистском жаргоне, Phemto – это легкий, автоматизированный контейнер dependency injection (управления зависимостями). Проще говоря, задача Phemto – создавать экземпляр объекта, получая минимум информации, таким образом, значительно ослабляя зависимости внутри приложения или фреймворка.
Проще всего понять паттерн DI это представить себе шкалу с «Используем DI» на одном конце и «Используем хардкодинг (т.е. жестко запрограммированные связи)» на другом. Мы с вами сейчас устроим маленькое путешествие от хардкодинга через паттерны Factory, Registry, Service Locator к DI. Если Вы и так знаете, что такое DI, переходите сразу к
установке Phemto.
Заурядное создание объектов с помощью оператора new выглядит простым и понятным, но мы, скорее всего, столкнемся с трудностями, когда захотим что-то поменять потом. Посмотрим на код…
Здесь MyController зависит от MysqlConnection.
Оператор new ясен и понятен, но MyController сможет использовать только БД MySQL. Немного переделать класс, чтобы было можно его наследовать едва ли поможет, т.к. тогда мы будем иметь в наследнике вместе с логикой дочернего контроллера и логику получения драйвера БД. В любом случае множественные зависимости не решаются наследованием, приводя к захламлению класса. Вообще говоря, Вы можете разыграть карту наследования только однажды.
Следующий шаг, – используем Factory…
class MyController <
function __construct ( $connection_pool ) <
.
$connection = $connection_pool -> getConnection ( ) ;
>
>
Очень эффективное решение. Фабрика может быть настроена на нужный тип драйвера с помощью конфигурационного файла или явно. Фабрики часто могут создавать объекты из разных семейств объектов, и тогда их называют Abstract Factory (Абстрактная Фаброика) или Repository (Репозиторий). Однако тут есть ограничения.
Фабрики приносят много дополнительного кода. Если надо тестировать классы с помощью mock-объектов, то придется имитировать не только сами, возвращаемые фабрикой объекты, но и саму фабрику. Получаете немного дополнительной суеты.
Да и в живом коде, если нужно вернуть объект, о котором автор фабрики не подумал, то придется наследовать или переписывать и саму фабрику, что для фреймворков может оказаться заметной проблемой.
Следующий ход в нашей борьбе с зависимостями, это вообще вынуть создание объекта Registry из основного объекта наружу…
class MyController <
function __construct ( $registry ) <
.
$connection = $registry -> connection ;
>
>
.
$registry = new Registry ( ) ;
$registry -> connection = new MysqlConnection ( ) ;
.
$controller = new MyController ( $registry ) ;
Registry совсем пассивен, зато в основном коде мы создаем и перегружаем много объектов. Мы даже можем случайно насоздавать про запас объектов, которые никогда не потребуются и так и оставить это место.
Кроме того, с помощью такого подхода мы не сможем использовать ленивое создание объектов (lazy loading). Неудача ждет нас, и если мы захотим, чтобы нам возвращался не один и тот же объект адаптера к БД, а разные объекты.
Жизнь сразу ухудшится, если в нашем примере будут еще зависимости, которые надо учесть. Т.е. если, например, для создания объекта-адаптера недостаточно сделать new, а нужно добавить в конструктор какой-то еще объект. В общем, предварительная настройка грозит сделаться весьма запутанной.
Мы можем сделать паттерн Registry более изощренным, если позволим объекту Registry самостоятельно создавать экземпляры нужных объектов. Наш объект стал Сервис-локатором (Service Locator)…
class MyController <
function __construct ( $services ) <
.
$connection = $services -> connection ;
>
>
.
$services = new ServiceLocator ( ) ;
$services -> connection ( ‘MysqlConnection’ ) ;
.
$controller = new MyController ( $services ) ;
Теперь настройки, могут быть в любом порядке, однако ServiceLocator должен знать, как создать MysqlConnection. Задача решается с помощью фабрик или с помощью трюков с рефлексией, хотя передача параметров, может стать весьма кропотливой работой. Жизненный цикл объектов (напр. возвращать один и тот же объект, или создавать разные) теперь под контролем программиста, который может как, запрограммировать все в методах фабрики, так и вынести все в настройки или плагины.
К сожалению, эта почти серебряная пуля имеет ту же проблему, что и Registry. Любой класс, который будет пользоваться таким интерфейсом, неизбежно будет зависеть от Сервис-локатора. Если Вы попробуете смешать две системы с разными сервис-локаторами, вы почувствуете что такое «не повезло».
Dependency Injection заходит немного с другой стороны. Посмотрим на наш самый первый пример…
… и сделаем зависимость внешней.
На первый взгляд, это просто ужасно. Теперь ведь каждый раз в скрипте придется все эти зависимости руками трогать. Изменить адаптер к БД придется вносить изменения в сотне мест. Так бы оно и было, если бы мы использовали new…
Хотите верьте, хотите нет, но это все, что нам нужно.
Задача Phemto – выявление того, как создать объект, что позволяет на удивление здорово автоматизировать разработку. Только по типу параметра в интерфейсе он выведет, что MysqlConnection – единственный кандидат, удовлетворяющий нужному типу Connection.
Более сложные ситуации, могут потребовать дополнительной информации, которая обычно содержится в «цепочечном» файле. Вот пример такого файла из реальной жизни, чтобы можно было почувствовать мощь паттерна…
$injector = new Phemto ( ) ;
$injector -> whenCreating ( ‘Page’ ) -> forVariable ( ‘session’ ) -> willUse ( new Reused ( ‘Session’ ) ) ;
$injector -> whenCreating ( ‘Page’ ) -> forVariable ( ‘continuation’ ) -> willUse ( ‘Continuation’ ) ;
$injector -> whenCreating ( ‘Page’ ) -> forVariable ( ‘alerts’ ) -> willUse ( ‘Alert’ ) ;
$injector -> whenCreating ( ‘Page’ ) -> forVariable ( ‘accounts’ ) -> willUse ( ‘Accounts’ ) ;
$injector -> whenCreating ( ‘Page’ ) -> forVariable ( ‘mailer’ ) -> willUse ( ‘Mailer’ ) ;
$injector -> whenCreating ( ‘Page’ ) -> forVariable ( ‘clock’ ) -> willUse ( ‘Clock’ ) ;
$injector -> whenCreating ( ‘Page’ ) -> forVariable ( ‘request’ ) -> willUse ( ‘Request’ ) ;
return $injector ;
Такое количество настроек типично для проекта среднего размера.
Теперь контроллер задает только интерфейс, а работа по созданию объектов выполняется посредником.
MyController теперь не должен вообще знать про MysqlConnection.
Зато $injector знает и о том и о другом. Это называется обращение контроля Inversion of Control.
Understanding Dependency Injection
Dependency injection and dependency injection containers are different things:
- dependency injection is a method for writing better code
- a container is a tool to help injecting dependencies
You don’t need a container to do dependency injection. However a container can help you.
PHP-DI is about this: making dependency injection more practical.
The theory
Classic PHP code
Here is how a code not using DI will roughly work:
- Application needs Foo (e.g. a controller), so:
- Application creates Foo
- Application calls Foo
- Foo needs Bar (e.g. a service), so:
- Foo creates Bar
- Foo calls Bar
- Bar needs Bim (a service, a repository, …), so:
- Bar creates Bim
- Bar does something
Using dependency injection
Here is how a code using DI will roughly work:
- Application needs Foo, which needs Bar, which needs Bim, so:
- Application creates Bim
- Application creates Bar and gives it Bim
- Application creates Foo and gives it Bar
- Application calls Foo
- Foo calls Bar
- Bar does something
This is the pattern of Inversion of Control. The control of the dependencies is inverted from one being called to the one calling.
The main advantage: the one at the top of the caller chain is always you. You can control all dependencies and have complete control over how your application works. You can replace a dependency by another (one you made for example).
For example what if Library X uses Logger Y and you want to make it use your logger Z? With dependency injection, you don’t have to change the code of Library X.
Using a container
Now how does a code using PHP-DI works:
- Application needs Foo so:
- Application gets Foo from the Container, so:
- Container creates Bim
- Container creates Bar and gives it Bim
- Container creates Foo and gives it Bar
- Foo calls Bar
- Bar does something
In short, the container takes away all the work of creating and injecting dependencies.
Understanding with an example
This is a real life example comparing a classic implementation (using new or singletons) VS using dependency injection.
Without dependency injection
class GoogleMaps < public function getCoordinatesFromAddress($address) < // calls Google Maps webservice >> class OpenStreetMap < public function getCoordinatesFromAddress($address) < // calls OpenStreetMap webservice >>
The classic way of doing things is:
class StoreService < public function getStoreCoordinates($store) < $geolocationService = new GoogleMaps(); // or $geolocationService = GoogleMaps::getInstance() if you use singletons return $geolocationService->getCoordinatesFromAddress($store->getAddress()); > >
Now we want to use the OpenStreetMap instead of GoogleMaps , how do we do? We have to change the code of StoreService , and all the other classes that use GoogleMaps .
Without dependency injection, your classes are tightly coupled to their dependencies.
With dependency injection
The StoreService now uses dependency injection:
class StoreService < private $geolocationService; public function __construct(GeolocationService $geolocationService) < $this->geolocationService = $geolocationService; > public function getStoreCoordinates($store) < return $this->geolocationService->getCoordinatesFromAddress($store->getAddress()); > >
And the services are defined using an interface:
interface GeolocationService < public function getCoordinatesFromAddress($address); >class GoogleMaps implements GeolocationService < . class OpenStreetMap implements GeolocationService < .
Now, it is for the user of the StoreService to decide which implementation to use. And it can be changed anytime, without having to rewrite the StoreService .
The StoreService is no longer tightly coupled to its dependency.
With PHP-DI
You may see that dependency injection has one drawback: you now have to handle injecting dependencies.
That's where a container, and specifically PHP-DI, can help you.
$geolocationService = new GoogleMaps(); $storeService = new StoreService($geolocationService);
$storeService = $container->get('StoreService');
and configure which GeolocationService PHP-DI should automatically inject in StoreService through configuration:
$container->set('StoreService', \DI\create('GoogleMaps'));
If you change your mind, there's just one line of configuration to change now.
Interested? Go ahead and read the Getting started guide!
A question? Unsatisfied with the documentation? Open an issue or chat on Gitter or Twitter.
Want to support my work? Check out Serverless Visually Explained.
- Foo calls Bar