Колупаем DI Container, Кто такой и с чем едят.
Немного теории. За одно по паттернам побежимся.
Эволюция развития IoC (Inversion of Control).
DI (Dependency Injection), это по сути передача объекта параметром.
Вот элементарный пример. Допустим есть класс-служба. Или сервис. Вобщем то, что часто нужно:
class Example { public function run() { echo 'cработало '; } } (new Example)->run();
Теперь есть второй класс, использующий его:
class Example { public function run() { echo 'cработало '; } }
class UsingClass { public function __construct() { $obj = new Example; echo 'и тут '; $obj->run(); } } (new UsingClass);
Но тут возникает проблема. Хардкод. Класс UsingClass жестко зависит от Example. Если потребуется изменить его (допустим для юнит-тестирования), то придется лезть в код. А это нарушение SOLID. А классов, использующих Example может быть много, и это будет адъ и израиль.
Решается это созданием фабрики:
// Фабрика class Factory { public $container = []; public function __construct() { $this->container['Example'] = new Example; // $this->container['SomeClass'] = new SomeClass; // $this->container['AnotherClass'] = new AnotherClass; } public function get($class) { return new $this->container[$class]; } } class Example { public function run() { echo 'cработало '; } } class UsingClass { public function __construct() { $obj = new Factory; echo 'и тут '; $obj->get('Example')->run(); } } (new UsingClass);
Уже лучше получилось. Подменить классы можно в одном месте. Однако в проекте может быть несколько разных фабрик. Инициализировать их все в методе, значит неимоверно утяжелять код. Можно создать фабрику фабрик. Это весело. :)
Кроме того, хардкод остался, хоть и в одном месте. Подменить класс не получится, без модификации кода класса, а это опять не по правилам. Но есть другое решение.
Легким движением руки фабрика превращается в абстрактную фабрику.
// Абстрактная фабрика class AbstractFactory { public $container = []; public function set($class) { $this->container[$class] = new $class; } public function get($class) { return new $this->container[$class]; } } class Example { public function run() { echo 'cработало '; } } class UsingClass { public function __construct() { $obj = new AbstractFactory; echo 'и тут '; $obj->set('Example'); $obj->get('Example')->run(); } } (new UsingClass);
Теперь можно подменить контейнер, и все классы одним махом заменятся на юнит-тесты допустим. Однако в таком случае мы опять нарушим принцип SOLID. Потому что для этого всё равно нужно будет залазить в код класса фабрики.
И вот тут наступает очередь DI. Мы отправляем весь объект фабрики в класс из контекста:
class AbstractFactory { public $container = []; public function instance($class) { $this->container[$class] = new $class; } public function get($class) { return $this->container[$class]; } } class Example { public function run() { echo 'cработало '; } } class UsingClass { public function __construct($factory) { echo 'и тут '; $factory->instance('Example'); $factory->get('Example')->run(); } } ////////////////////////////////////////// //Отправляем весь объект фабрики прямо в конструктор. (new UsingClass(new AbstractFactory));
Это называется "внедрение зависимости через конструктор", или "Constructor Injection". Еще можно внедрять объекты через свойства или сеттеры. Не суть. Суть в том, что теперь не нужно трогать сами классы. Достаточно подменить всю фабрику при вызове.
Но все равно код получается сильно связанным. Опять нарушен один из пунктов SOLID. Потому что работа UsingClass зависит от конкретных классов. А хотелось бы повторного использования.
Так вот, раз мы отправляем эту фабрику из контекста, то можно и настроить её в том же месте. А это мы полную бочку туда катим. Это уже контейнерная перевозка получается. :)
Такая контейнерная перевозка называется DI-контейнер. Самая простая реализация:
class DiContainer { public $container = []; public function set($key, $class) { $this->container[$key] = $class; } public function get($key) { return new $this->container[$key]; } } class Example { public function run() { echo 'cработало '; } } class UsingClass { public function __construct($container) { echo 'и тут '; $container->get('work')->run(); } } ////////////////////////////////////////// $container = new DiContainer; $container->set('work', 'Example'); (new UsingClass($container));
Смысл в том, что теперь можно набить контейнер службами (объектами или интерфейсами библиотек), а инстанцировать их только в момент обращения, создав централизацию управления и не заботясь о лишнем потреблении ресурсов. Кроме того, соблюдается принцип инверсии контроля. Класс UsingClass может работать теперь и с другими классами. Вроде бы одни плюсы...
Но не все так гладко.
Гладко было на бумаге, да забыли про овраги. Такая реализация хороша тогда, когда внедряемый объект изначально готов к использованию и не требует предварительных настроек. Но довольно часто нужно при инициализации объекта передать ему какие то парамеры. А как это сделать, если объект создается только при обращении к нему, непосредственно в использующем контейнер классе?
Эволюция продолжается. Теперь мы будем хранить не идентификатор (в моем примере это "work"), а анонимную функцию, которая при обращении к контейнеру создаст объект с уже подготовленными параметрами. Самым показательным является пример с коннектом. Нужно же как то передать пароль, пользователя, название базы.
class DiContainer { public $container = []; public function set($key, $callable) { $this->container[$key] = $callable; } public function get($key) { return call_user_func($this->container[$key]); } } class DB { public $connect; public function __construct($data) { // тут как бы коннект $this->connect = 'PDO: '. implode(';', $data); } } class UsingClass { public function __construct($container) { $db = $container->get('db'); echo $db->connect; } } ////////////////////////////////////////// $container = new DiContainer; $container->set('db', function () { return new DB( array( 'host' => 'localhost', 'username' => 'root', 'password' => 'qwerty', 'dbname' => 'some_base' ) ); }); (new UsingClass($container));
На самом деле это вовсе не DI-контейнер. Это обычный сервис-локатор. Хотя разница между ними на первый взгляд небольшая, поэтому многие путают одно с другим. Настоящий контейнер, это служба, которая сама разбирается с нужными сервисами. Контейнер должен быть "запрограммирован" так, чтобы мог сам генерировать вложенные зависимости.
Реализация контекста еще больше становится сложной, на лицо начало оверинжениринга и подмена синтаксиса. Некоторые контейнеры используют ArrayAccess, и тогда синтаксис вообще улетает в сторону письменности майя:
$container['db'] = function () { return new DB( array( 'host' => 'localhost', 'username' => 'root', 'password' => 'qwerty', 'dbname' => 'some_base' ) ); });
С какого перепуга тут взялся массив... Если инициализация объекта $container рядом, мжно интуитивно понять, что имеется ввиду. Но если человек не сталкивался с DI-container и ArrayAccess раньше, то это пердимонокль.
Кроме того, сам контейнер должен уметь выполнять кучу дополнительных плюшек, таких как валидация аргументов, проверка доступности, глобальный доступ, клонирование, инстанцирование с данными из файла (конфиги допустим), постустановка параметров, и пошло, пошло, поехало.
Тут две проблемы. Первая - дополнительный синтаксис и соглашения. Для того, чтобы начать полноценно использовать DI-контейнер, пользователю фреймворка придется перлопатить и запомнить кучу информации из доки.
И вторая. Ресурс. Я посмотрел реализацию сервиса служб у symfony, и мне стало дурно. Это сколько нужно тащить в память, чтобы внедрить контейнером элементарный коннект. :blink:
Некоторые решения основываются на рефлексии, как у ZEND к прмеру. Но даже они сами предупреждают, что использование этой фичи полностью ложится на совесть разработчика, так как нещадно жрет ресурс и время.
Но это для настоящего tru-программиста вовсе не помеха. Он давно уже положил на это болт с крупной конусной резьбой. Ведь мы живем в век быстродействия и железо стоит копейки. Однако забывая, что многие шаред-хостинги ограничивают потребление ресурса.
Тогда придумали кэширование, что еще больше усложняет конструкцию. И так далее и тому подобное. Вобщем Остапа понесло. :) C изобретением DI-контейнера произошел резкий скачек в эволции ООП. Но помоему это скачек в сторону. :)
Резюме.
Но вот в чем парадокс на мой взгляд. Для чего вообще нужна инверсия зависимостей с помощью контейнера? Вот две основные причины:
1. Слабая связанность проекта
2. Легкость тестирования.
И если с первым пунктом можно согласиться, когда создается не модульная (сервисная), а монолитная архитектура. С высокой степенью абстракции и полиморфизма. Когда изменения в каком-нибудь классе может аукнуться на другом конце системы.
В сервисной архитектуре это не обязательный пункт за. Потому что модуль должен быть самодостаточным и не использовать другие модули. А значит нечего и некуда внедрять за его пределами. Внутри же вполне можно обойтись более простыми паттернами, как тот же сервис-локатор.
А со вторым - отдельная история. Это же чистейшее нарушение принципа KISS. используя DI-container для облегчения тестирования, мы переворачиваем все с ног на голову. Усложняем систему, которая будет работать годами, ради упрощения тестов, которые требуются только при разработке и рефакторинге. Очевидно и логично потратить больше времени на изготовление тестов, чем на реализацию самого проекта.