Этот урок переехал в мой гитхаб по адресу: https://github.com/codedokode/pasta/blob/master/arch/di.md
Ниже устаревшая версия урока.
Проблема, которую мы решаем — связность классов. Если в классе A написано
$b = new B;
$c = B::getC();
то мы получаем жестко прописанную зависимость A от B на которую не можем повлиять никак (сильную связанность). Мы не можем подсунуть классу A что-то другое вместо B. Мы не можем распространять его отдельно от B.
Пример: если класс, который через PDO
что-то делает с базой данных. Мы бы хотели подменить PDO
на наш класс, который печатает все выполняемые запросы и время их выполнения. Но мы не можем это сделать из-за неграмотной архитектуры. Печаль.
Другой пример: для тестирования мы хотим подменить реальную базу на класс-заглушку. Это тоже невозможно.
Третий пример: наш телефон можно заряжать только идущей в комплекте зарядкой (допустим он проверяет серийный номер). Она белая с яблочком. Мы бы хотели использовать розовую, или зарядку с более длинным проводом да еще и более дешевую, но увы, они не подходят.
В то же время, в ООП есть средства решения проблемы: использование интерфейсов или передача класса-наследника. Но в данной архитектуре они становятся недоступны.
Решения:
Не рассматривается
Cоздается класс-массив со статическими методами, в который можно положить объект по имени и достать его:
Registry::set('db', new PDO);
// ... пишем в классе A....
$db = Registry::get('db');
Это почти то же, что глобальный массив — с той разницей что ты можешь вписать какой-то хитрый код в get
/set
например для проверки правильности названия.
Теперь мы можем вместо класса B
засунуть в Registry
что-то другое.
Но в общем это плохое решение. Почему? Потому что засунув что-то другое в Registry
, оно будет использовано всеми экземплярами класса A
. Мы не можем создать один экземпляр с одним значением db
, а другой с другим. Теперь у нас будут логгироваться все запросы, а не только запросы которые делает один класс. То есть опять же теряем возможность ООП создавать сколько угодно объектов с разными настройками.
Также, Registry
никак не проверяет что мы в него засунули. Это тоже нехорошо так как лучше бы он сразу писал про ошибку чем она выяснится в день сдачи проекта.
Registry
не документируется. Ты не знаешь что в нем хранится, пока не просмотришь весь код.
Также, мы не можем иметь 2 и более registry из-за статических методов. Например, мы не можем создать registry с объектами-заглушками для тестов (да, многие из проблем о которых я пишу всплывают именно при попытке наладить тестирование).
Тем не менее, этот паттерн примеряется из-за своей простоты, смотри ZF например: http://framework.zend.com/manual/1.12/ru/zend.registry.using.html
Более интересная штука, но в плане эволюции она недалеко ушла от Registry
. Идея в том, что мы создаем класс, в нем много методов, каждый из которых возвращает какую-то зависимость:
class ServiceLocator
{
public function setDb(PDO $db)
{
$this->db = $db;
}
public function getDb()
{
return $this->db;
}
}
Альтернативная версия SL без сеттеров:
public function getDb()
{
return new PDO;
}
Код в классе A теперь выглядит так:
construct($serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
....
$db = $this->serviceLocator->getDb();
SL определенно лучше Registry. Во-первых, мы видим список всех функций в нем и понимаем какие в нем есть сервисы. Во-вторых, он не использует статические методы и мы можем создать несколько SL с разными настройками и соответственно несколько экхемпляров класса A.
Но он по-прежнему обладает недостатками:
- у класса
A
теперь есть зависимость отServiceLocator
(хотя тот ему нужен лишь для получения нескольких объектов). Ты не можешь распространять классA
отдельно. - не очевидно, какие сервисы использует класс
A
без полного изучения его кода. SL
отравляет код. Если ты хочешь использовать класс, которому нуженSL
, ты тоже должен его начать хранить. Так зависимость отSL
расползается по всем классам проекта, словно вирус.
Решением этих проблем является DI/IoC (инверсия управления/внедренеи зависимостей). До сих пор у нас класс A
сам искал и получал нужные ему зависимости:
$b = new B();
...
$db = $this->sl->getDb();
В случае с DI зависимости внедряются (инжектируются, впрыскиваются) в класс извне. Это можно сделать через конструктор класса А
:
function construct(DB $db, Logger $logger, Something $smth)
{
$this->db = $db
....
Либо через метод-сеттер в классе А
:
function setDb(DB $db)
{
$this->db = ...
Либо как-то через интерфейс, я не разбирался как так как это непринципиально.
Конструктор используется для обязательных зависимостей, сеттер для опциональных. Обрати вниамние, где здесь инверсия управления (IoC): класс A
больше не ищет зависимости, их в него внедряют снаружи. Код поиска зависимостей удалось вынести из класса (он был там не нужен с самого начала!) и он теперь не связан сильно ни с DB
, ни с Registry
, ни с ServiceLocator
. Ты можешь использовать его как хочешь.
Остается небольшая проблема: теперь чтобы создать класс A мы должны писать много букв:
$db = new DB();
$logger = new Logger(.....);
$smth = new Smth(...);
$a = new A($db, $logger, $smth);
Это уныло. Потому в фреймворках есть решения. В Симфони 2 используется DI container: ты описываешь зависимости класса в конфиге, либо кодом, либо аннотациями в коде и при вызове $container->get('a')
он создает экземпляр по описанным правилам. Инфо:
http://symfony.com/doc/current/components/dependency_injection/introduction.html
Кроме объектов, разумеется можно передавать и числа/строки как дополнительные настройки.
Заметь что контейнер — внешняя вешь по отношению к A
. Мы не передаем сам контейнер в A
(иначе это будет ServiceLocator
). Класс А
от него не зависит, мы можем в любой момент выкинуть контейнер и создать объект руками или взять другой контейнер от другого производителя. Мы можем описать в конфиге и создать несколько экземпляров A
с разными настройками. Ты чувствуешь силу ООП и зришь свет разума, падаван?
В ZF2 настройка контейнера возможна еще автоматически: специальный код умеет читать тайпхинты в конструкторе (DB $db
) и сам догадывается объект какого класса ему нужен:
Наконец, добавлю еще мелочь: вместо указания классов лучше может быть указывать интерфйесы: не construct(DB $db)
а construct (DBInterface $db)
. Почему? Потому что в первом случае мы можем передать только DB или его наследника, а во втором случае мы можем передать любой класс, поддерживающий интерфейс. Например, класс-заглушка для тестов.
В общем, все эти сложности и непонятные слова служат одной цели: мы хотим использовать все возможности ООП и писать программу как набор повторно используемых и взаимозаменяемых модулей, которые можно соединять между собой. Чтобы мы могли заменить модуль работы с БД на другой, не трогая остальной код. Если проводить аналогии, то правильный ООП — это как универсальная зарядка: любая зарядка может заряжать любой телефон, если у них имеется определенной формы разъем. Или как разъем монитора: ты можешь подключить к компьютеру мониторы разных типов и даже с разным числом пикселей — и все будет работать. Я думаю, любому очевидно что единый стандарт лучше множества закрытых несовместимых решений, верно?
Ты можешь еще попытаться возразить «но зачем городить весь этот DI, если мне надо логгировать запросы, я просто впишу пару строчек в класс DB. Или опцию в конфиге.». Предлагаю контраргументы тебе привести самому.
Все эти штуки описал и разложил по полочкам Фаулер (он очень умный) в своей статье: http://www.martinfowler.com/articles/injection.html (англ) Переводы на русский:
Пишите еще.