How to start using DI

I repeatedly came across the opinion that DI is something complex, bulky, slow, suitable only for "large" projects, and therefore its use specifically on the current task (500+ model classes, 300+ controller classes) is unjustified. This is partly due to the fact that DI is unambiguously associated with packages like Symfony's “The Dependency Injection Component”, which obviously covers all possible dependency injection options.
Here I want to give a certain functional minimum, which will give an understanding of the concept itself, in order to show that the inversion of dependencies itself can be quite simple and concise.

Content


The implementation is 2 classes of 500 lines of code:
SimpleDi \ ClassManager - provides information about the classes. For full-fledged work, it needs a cache (we use Doctrine \ Common \ Cache \ ApcCache), this will allow us not to create reflections with every call to the script. Parses annotations for subsequent injection. It can also be used in the bootloader, as it stores the path to the class file.
SimpleDi \ ServiceLocator - creates and initializes the services requested from it. It is this class that makes injections.
1) In the simplest case, when no settings are set for the class, SimpleDi \ ServiceLocator works similarly to the multiton pattern (aka Object Pool).
$service_locator->get('HelperTime');

2) Implementation option through the field
class A
{
    /**
     * @Inject("HelperTime")
     * @var HelperTime
     */
   protected $helper_time;
}
$service_locator->get('A');

This option should be used exclusively in controllers, as a reflection will be created for implementation, which affects performance for the worse. One class will not affect the call of a script with several fields during page loading, but if you use it everywhere, the performance loss will be quite noticeable.
Here I want to make a digression towards Symfony. There, a similar implementation is permissible:
  • in controllers for fields with any visibility (including protected, private), and this is due precisely to an insignificant impact on performance, and besides this, the controller itself is a container of services (and has a get () method similar to our ServiceLocator :: get ());
  • in any classes (services) for public fields, as in this case no reflection will be created, and a simple assignment of $ service-> field = $ injected_service will be used, which will lead to an exception for private / protected fields.

In our implementation, reflection is always created, implementation will always end successfully.
3) Implementation through the method
class B 
{
    /**
     * @var HelperTime
     */
    protected $helper_time;
    /**
     * @Inject("HelperTime")
     * @param HelperTime $helper
     */
    public function setHelperTime($helper)
    {
        $this->helper_time = $helper;
    }
}
$service_locator->get('B');

This option is most acceptable and should be used along with the implementation through the field to install dependencies by default.
4) Deployment through the config
$service_locator->setConfigs(array(
    'class_b_service' => array(
        'class' => 'B',
        'calls' => array(
            array('setHelperTime', array('@CustomHelperTime')),
        )
    )
));
$service_locator->get('class_b_service');

This is what dependency injection is used for. Now, through the settings, it is possible to replace the helper used in class B, while class B itself will not change.
5) Create a new instance of the class. When you need to have several objects of the same class, you can use ServiceLocator as a factory
$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);
}


Example


Take an arbitrary useful library and try to implement it in our project. Let's say this is github.com/yiisoft/yii/blob/master/framework/utils/CPasswordHelper.php
It turns out we cannot do this, because the class is rigidly tied to the absolutely unnecessary classes Yii and 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('_'=>'.','~'=>'/'));
    }
}

In order to make the class available for any project, it would be enough to correctly describe the dependencies:
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('_'=>'.','~'=>'/'));
    }
}

And get a class - an exception generator
class YiiExceptor
{
    public function create($a, $b, $c = null)
    {
        throw new CException(Yii:t($a, $b, $c));
    }
}


Conclusion


Using DI allows you not to think about in what context your module will be used. It makes it possible to transfer a separate class to another project without a set of (often hierarchical) dependencies. When using annotations you do not have to deal with the explicit creation of objects and the explicit transfer of parameters and services to the object. And, of course, such a class is at times easier to test than tied to static methods or explicitly creating instances of the class, instead of using the factory.

References


The example itself is github.com/mthps/SimpleDi
Theory en.wikipedia.org/wiki/Dependency_Injection
One of the best implementations of symfony.com/doc/current/components/dependency_injection/index.html

Also popular now: