Как начать использовать DI

от автора

Многократно сталкивался с мнением, что DI это нечто сложное, большое, медленное, подходящее только для «больших» проектов, а потому его использование конкретно на текущей задаче (500+ классов моделей, 300+ классов контроллеров) неоправданно. Отчасти это связано с тем, что DI однозначно ассоциируется с пакетами вроде Symfony «The Dependency Injection Component», заведомо с лихвой покрывающими все возможные варианты внедрения зависимостей.
Здесь я хочу привести некий функциональный минимум, который даст понимание самой концепции, дабы показать, что сама инверсия зависимостей может быть достаточно проста и лаконична.

Содержание

Реализация составляет 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *