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

в 13:04, , рубрики: Без рубрики

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

Содержание

Реализация составляет 2 класса из 500 строк кода:
SimpleDiClassManager – предоставляет информацию о классах. Для полноценной работы ему необходим кэшер (мы используем DoctrineCommonCacheApcCache), это позволит не создавать отражений при каждом вызове скрипта. Разбирает аннотации для последующей инъекции. Так же его возможно использовать в загрузчике, т.к. он хранит путь до файла класса.
SimpleDiServiceLocator – создает и инициализирует запрашиваемые у него сервисы. Именно этот класс производит инъекции.
1) В простейшем случае, когда для класса не заданы никакие настройки, SimpleDiServiceLocator работает аналогично паттерну 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 YiiSecurityManager
     */ 
    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

Автор: mthps

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js