依赖注入(DI)在PHP中的实现

什么是依赖注入?

IOC:英文全称:Inversion of Control,中文名称:控制反转,它还有个名字叫依赖注入(Dependency Injection,简称DI)。php

当一个类的实例须要另外一个类的实例协助时,在传统的程序设计过程当中,一般由调用者来建立被调用者的实例。而采用依赖注入的方式,建立被调用者的工做再也不由调用者来完成,所以叫控制反转,建立被调用者的实例的工做由IOC容器来完成,而后注入调用者,所以也称为依赖注入。redis

举个简单的例子:mongodb

(1)原始社会里,几乎没有社会分工。须要斧子的人(调用者)只能本身去磨一把斧子(被调用者)。数据库

(2)进入工业社会,工厂出现。斧子再也不由普通人完成,而在工厂里被生产出来,此时须要斧子的人(调用者)找到工厂,购买斧子,无须关心斧子的制造过程。编程

(3)进入“按需分配”社会,须要斧子的人不须要找到工厂,坐在家里发出一个简单指令:须要斧子。斧子就天然出如今他面前。缓存

第一种状况下,实例的调用者建立被调用的实例,必然要求被调用的类出如今调用者的代码里。没法实现两者之间的松耦合。框架

第二种状况下,调用者无须关心被调用者具体实现过程,只须要找到符合某种标准(接口)的实例,便可使用。此时调用的代码面向接口编程,可让调用者和被调用者解耦,这也是工厂模式大量使用的缘由。但调用者须要本身定位工厂,调用者与特定工厂耦合在一块儿。运维

第三种状况下,调用者无须本身定位工厂,程序运行到须要被调用者时,依赖注入容器自动提供被调用者实例。事实上,调用者和被调用者都处于依赖注入容器的管理下,两者之间的依赖关系由依赖注入容器提供。所以调用者与被调用者的耦合度进一步下降,这使得应用更加容易维护,这就是依赖注入所要达到的目的。性能

用php实现一个轻量的依赖注入容器

首先咱们建立一个类,看起来是这样的:this

<?php   
class Di
{
    protected $_service = [];
    public function set($name, $definition)
    {
        $this->_service[$name] = $definition;
    }
    public function get($name)
    {
        if (isset($this->_service[$name])) {
            $definition = $this->service[$name];
        } else {
            throw new Exception("Service '" . name . "' wasn't found in the dependency injection container");
        }

        if (is_object($definition)) {
            $instance = call_user_func($definition);
        }

        return $instance;
    }
}

如今咱们已经有了一个简单的类,包含一个属性和两个方法。假设咱们如今有两个类,redisDB和cache,redisDB提供一个redis数据库的操做,cache负责缓存功能的实现而且依赖于redisDB。

class redisDB
{
    protected $_di;

    protected $_options;

    public function __construct($options = null)
    {
        $this->_options = $options;
    }

    public function setDI($di)
    {
        $this->_di = $di;
    }

    public function find($key, $lifetime)
    {
        // code
    }

    public function save($key, $value, $lifetime)
    {
        // code
    }

    public function delete($key)
    {
        // code
    }
}

在这个类中咱们简单实现了redis的查询、保存和删除。你可能会有疑问,另一个方法setDi是作什么的。待我继续为你讲解。另外一个类和当前这个类结构很像:

class cache
{
    protected $_di;

    protected $_options;

    protected $_connect;

    public function __construct($options = null)
    {
        $this->_options = $options;
    }

    public function setDI($di)
    {
        $this->_di = $di;
    }

    protected function _connect()
    {
        $options = $this->_options;
        if (isset($options['connect'])) {
            $service = $options['connect'];
        } else {
            $service = 'redis';
        }

        return $this->_di->get($service);
    }

    public function get($key, $lifetime)
    {
        $connect = $this->_connect;
        if (!is_object($connect)) {
            $connect = $this->_connect()
            $this->_connect = $connect;
        }
        // code
        ...
        return $connect->find($key, $lifetime);
    }

    public function save($key, $value, $lifetime)
    {
        $connect = $this->_connect;
        if (!is_object($connect)) {
            $connect = $this->_connect()
            $this->_connect = $connect;
        }
        // code
        ...
        return $connect->save($key, $lifetime);
    }

    public function delete($key)
    {
        $connect = $this->_connect;
        if (!is_object($connect)) {
            $connect = $this->_connect()
            $this->_connect = $connect;
        }
        // code
        ...
        $connect->delete($key, $lifetime);
    }
}

如今咱们就当已经实现了redisDB和cache这两个组件,具体的细节这里就先不作讨论了,来看看如何使用使用吧。首先须要将两个组件注入到容器中:

<?php
    $di = new Di();
    $di->set('redis', function() {
         return new redisDB([
             'host' => '127.0.0.1',
             'port' => 6379
         ]);
    });
    $di->set('cache', function() use ($di) {
        $cache = new cache([
            'connect' => 'redis'
        ]);
        $cache->setDi($di);
        return $cache;
    });


    // 而后在任何你想使用cache的地方
    $cache = $di->get('cache');
    $cache->get('key'); // 获取缓存数据
    $cache->save('key', 'value', 'lifetime'); // 保存数据
    $cache->delete('key'); // 删除数据

到这里你可能会以为这样以来反而有点繁琐了。cache和redisDB的结构如此之像,彻底能够把redis写到cache中而不必单独分离出来?可是你想过没有,有些数据及时性没那么高并且数量比较大,用redis有点不合适,mongodb是更好的选择;有些数据更新频率更慢,对查询速度也没要求,直接写入文件保存到硬盘可能更为合适;再或者,你的客户以为redis运维难度有点大,让你给他换成memcache... 这就是为何把它分离出来了。而后,继续改进代码:

interface BackendInterface {
    public function find($key, $lifetime);
    public function save($key, $value, $lifetime);
    public function delete($key);
}

class redisDB implements BackendInterface
{
    public function find($key, $lifetime) { }
    public function save($key, $value, $lifetime) { }
    public function delete($key) { }
}

class mongoDB implements BackendInterface
{
    public function find($key, $lifetime) { }
    public function save($key, $value, $lifetime) { }
    public function delete($key) { }
}

class file implements BackendInterface
{
    public function find($key, $lifetime) { }
    public function save($key, $value, $lifetime) { }
    public function delete($key) { }
}

$di = new Di();
//  redis
$di->set('redis', function() {
     return new redisDB([
         'host' => '127.0.0.1',
         'port' => 6379
     ]);
});
// mongodb
$di->set('mongo', function() {
     return new mongoDB([
         'host' => '127.0.0.1',
         'port' => 12707
     ]);
});
// file
$di->set('file', function() {
     return new file([
         'path' => 'path'
     ]);
});
// save at redis
$di->set('fastCache', function() use ($di) {
     $cache = new cache([
         'connect' => 'redis'
     ]);
     $cache->setDi($di);
     return $cache;
});
// save at mongodb
$di->set('cache', function() use ($di) {
     $cache = new cache([
         'connect' => 'mongo'
     ]);
     $cache->setDi($di);
     return $cache;
});
// save at file
$di->set('slowCache', function() use ($di) {
     $cache = new cache([
         'connect' => 'file'
     ]);
     $cache->setDi($di);
     return $cache;
});

// 而后在任何你想使用cache的地方 
$cache = $di->get('cache');

咱们新增长了一个接口BackendInterface,规定了redisDB,mongoDB,file这三个类必须实现这个接口所要求的功能,至于其余锦上添花的功能,随你怎么发挥。而cache的代码,好像没有变,由于cache不须要关心数据是怎么存入数据库或者文件中。而cache的调用者,也不须要关心cache具体是怎么实现的,只要根据接口实现相应的方法就好了。多人协做你会更加受益,大家只须要商定好接口,而后分别实现就好了。

这就是依赖注入的魅力所在了,虽然看似如此简单。

以上代码还能够继续改进,直到你认为无可挑剔为止。好比,redis服务在一个请求中可能会调用屡次,而每次调用都会从新建立,这将有损性能。只需扩展一下DI容器就好增长一个参数或增长一个方法,随你。

class Di
{
    protected $_service = [];
    protected $_sharedService = [];
    public function set($name, $definition, $shared = false)
    {
        if ($shared) {
            $this->_sharedService[$name] = $definition;
        } else {
            $this->_service[$name] = $definition;
        }
    }
    public function get($name) {
        if (isset($this->_service[$name])) {
            $definition = $this->service[$name];
        } else if ($this->_sharedService[$name]) {
             $definition = $this->_sharedService[$name];
        } else {
            throw new Exception("Service '" . name . "' wasn't found in the dependency injection container");
        }
        ...
    }

这样以来,若是某个服务在一次请求中要调用屡次,你就能够将shared属性设置为true,以减小没必要要的浪费。若是你以为每次在注入时都要setDi有点繁琐,想让他自动setDi,那能够这么作:

interface DiAwareInterface
{
    public function setDI($di);
    public function getDI();
}

class Di
{
    protected $service;

    public function set($name, $definition)
    {
        $this->service[$name] = $definition;
    }

    public function get($name)
    {
        ...
        if (is_object($definition)) {
            $instance = call_user_func($definition);
        }

        // 若是实现了DiAwareInterface这个接口,自动注入
        if (is_object($instance)) {
            if ($instance instanceof DiAwareInterface) {
                $instance->setDI($this);
            }
        }

        return $instance;
    }
}

class redisDB implements BackendInterface, DiAwareInterface
{
    public function find($key, $lifetime) { }
    public function save($key, $value, $lifetime) { }
    public function delete($key) { }
}

而后,就能够这样:

$di->set('cache', function() {
    return new cache([
        'connect' => 'mongo'
    ]);
});

咱们如今所实现的这个DI容器还很简陋,还不支持复杂的注入,你能够继续完善它。

不过,经过这些代码你已经了解什么是依赖在注入了,你能够将这种思想应用到你的项目中,或者着手开发你本身的框架。

相关文章
相关标签/搜索