Здесь я хочу привести некий функциональный минимум, который даст понимание самой концепции, дабы показать, что сама инверсия зависимостей может быть достаточно проста и лаконична.
Содержание
Реализация составляет 2 класса из 500 строк кода:
SimpleDi\ClassManager – предоставляет информацию о классах. Для полноценной работы ему необходим кэшер (мы используем Doctrine\Common\Cache\ApcCache), это позволит не создавать отражений при каждом вызове скрипта. Разбирает аннотации для последующей инъекции. Так же его возможно использовать в загрузчике, т.к. он хранит путь до файла класса.
SimpleDi\ServiceLocator – создает и инициализирует запрашиваемые у него сервисы. Именно этот класс производит инъекции.
1) В простейшем случае, когда для класса не заданы никакие настройки, SimpleDi\ServiceLocator работает аналогично паттерну multiton (он же Object Pool).
$service_locator->get('HelperTime');
2) Вариант внедрения через поле
class A { /** * @Inject("HelperTime") * @var HelperTime */ protected $helper_time; } $service_locator->get('A');
Такой вариант следует использовать исключительно в контроллерах, т.к. для внедрения будет создано отражение, что влияет на производительность в худшую сторону. Один класс на вызов скрипта с несколькими полями никак на время загрузки страницы не повлияют, но если это использовать повсеместно, потеря производительности будет вполне ощутима.
Здесь хочется сделать отступление в сторону Symfony. Там подобное внедрение допустимо:
- в контроллерах для полей с любой видимостью (в том числе protected, private) и это объясняется именно незначительным влиянием на производительность, а кроме такого сам контроллер является контейнером сервисов (и имеет метод get() аналогичный нашему ServiceLocator::get());
- в любых классах (сервисах) для public полей, т.к. в этом случае не будет создаваться отражения, и будет использоваться простое присвоение $service->field = $injected_service, что для private/protected полей приведет к исключению.
В нашей реализации отражение создается всегда, внедрение всегда будет заканчиваться успешно.
3) Внедрение через метод
class B { /** * @var HelperTime */ protected $helper_time; /** * @Inject("HelperTime") * @param HelperTime $helper */ public function setHelperTime($helper) { $this->helper_time = $helper; } } $service_locator->get('B');
Такой вариант наиболее приемлем и наравне с внедрением через поле следует использовать для установки зависимостей по умолчанию.
4) Внедрение через конфиг
$service_locator->setConfigs(array( 'class_b_service' => array( 'class' => 'B', 'calls' => array( array('setHelperTime', array('@CustomHelperTime')), ) ) )); $service_locator->get('class_b_service');
Это то, для чего и используется внедрение зависимостей. Теперь через настройки возможно подменить используемый в классе B хелпер, при этом сам класс B изменяться не будет.
5) Создание нового экземпляра класса. Когда необходимо иметь несколько объектов одного класса, возможно использование ServiceLocator в качестве фабрики
$users_factory = $service_locator; $users_row = array( array('id' => 1, 'name' => 'admin'), array('id' => 2, 'name' => 'guest'), ); $users = array(); foreach ($users_rows as $row) { $user = $users_factory->createService('User'); $user->setData($row); }
Пример
Возьмем произвольную полезную библиотеку и попробуем внедрить в наш проект. Допустим это github.com/yiisoft/yii/blob/master/framework/utils/CPasswordHelper.php
Оказывается, мы не можем это сделать, потому что класс жестко завязан на абстолютно ненужные нам классы Yii и CException.
class CPasswordHelper { … public static function generateSalt($cost=13) { if(!is_numeric($cost)) throw new CException(Yii::t('yii','{class}::$cost must be a number.',array('{class}'=>__CLASS__))); $cost=(int)$cost; if($cost<4 || $cost>31) throw new CException(Yii::t('yii','{class}::$cost must be between 4 and 31.',array('{class}'=>__CLASS__))); if(($random=Yii::app()->getSecurityManager()->generateRandomString(22,true))===false) if(($random=Yii::app()->getSecurityManager()->generateRandomString(22,false))===false) throw new CException(Yii::t('yii','Unable to generate random string.')); return sprintf('$2a$%02d$',$cost).strtr($random,array('_'=>'.','~'=>'/')); } }
Для того, чтобы сделать класс доступным для любого проекта, достаточно было бы правильно описать зависимости:
class CPasswordHelper { /** * Здесь я для краткости воспользуюсь public полями, вряд ли в данном случае это большее зло, * чем вызов статических методов. * @Inject * @var \Yii\SecurityManager */ public $securityManager; /** * Генератор ошибок * @Inject * @var \YiiExceptor */ public $exceptor; … public function generateSalt($cost=13) { if(!is_numeric($cost)) $this->exceptor->create('yii','{class}::$cost must be a number.',array('{class}'=>__CLASS__)); $cost=(int)$cost; if($cost<4 || $cost>31) $this->exceptor->create('yii','{class}::$cost must be between 4 and 31.',array('{class}'=>__CLASS__)); if(($random=$this->securityManager->generateRandomString(22,true))===false) if(($random=$this->securityManager()->generateRandomString(22,false))===false) this->exceptor->create('yii','Unable to generate random string.'); return sprintf('$2a$%02d$',$cost).strtr($random,array('_'=>'.','~'=>'/')); } }
И завести класс – генератор исключений
class YiiExceptor { public function create($a, $b, $c = null) { throw new CException(Yii:t($a, $b, $c)); } }
Заключение
Использование DI позволяет не задумываться над тем, в каком контексте будет использоваться ваш модуль. Дает возможность переносить отдельный класс в другой проект без набора (часто иерархического) зависимостей. При использовании аннотаций вам не придётся заниматься явным созданием объектов и явной передачей параметров и сервисов в объект. И, конечно, такой класс в разы проще поддается тестированию, нежели завязанный на статические методы или явно создающий экземпляры класса, вместо использования фабрики.
Ссылки
Сам пример github.com/mthps/SimpleDi
Теория ru.wikipedia.org/wiki/Внедрение_зависимости
Одна из лучших реализаций symfony.com/doc/current/components/dependency_injection/index.html
ссылка на оригинал статьи http://habrahabr.ru/post/191168/
Добавить комментарий