依賴注入(DI)在PHP中的實現

什麼是依賴注入?

IOC:英文全稱:Inversion of Control,中文名稱:控制反轉,它還有個名字叫依賴注入(Dependency Injection,簡稱DI)。php

當一個類的實例須要另外一個類的實例協助時,在傳統的程序設計過程當中,一般由調用者來建立被調用者的實例。而採用依賴注入的方式,建立被調用者的工做再也不由調用者來完成,所以叫控制反轉,建立被調用者的實例的工做由IOC容器來完成,而後注入調用者,所以也稱爲依賴注入。git

舉個簡單的例子:github

(1)原始社會裏,幾乎沒有社會分工。須要斧子的人(調用者)只能本身去磨一把斧子(被調用者)。redis

(2)進入工業社會,工廠出現。斧子再也不由普通人完成,而在工廠裏被生產出來,此時須要斧子的人(調用者)找到工廠,購買斧子,無須關心斧子的製造過程。mongodb

(3)進入「按需分配」社會,須要斧子的人不須要找到工廠,坐在家裏發出一個簡單指令:須要斧子。斧子就天然出如今他面前。數據庫

第一種狀況下,實例的調用者建立被調用的實例,必然要求被調用的類出如今調用者的代碼裏。沒法實現兩者之間的鬆耦合。編程

第二種狀況下,調用者無須關心被調用者具體實現過程,只須要找到符合某種標準(接口)的實例,便可使用。此時調用的代碼面向接口編程,可讓調用者和被調用者解耦,這也是工廠模式大量使用的緣由。但調用者須要本身定位工廠,調用者與特定工廠耦合在一塊兒。緩存

第三種狀況下,調用者無須本身定位工廠,程序運行到須要被調用者時,依賴注入容器自動提供被調用者實例。事實上,調用者和被調用者都處於依賴注入容器的管理下,兩者之間的依賴關係由依賴注入容器提供。所以調用者與被調用者的耦合度進一步下降,這使得應用更加容易維護,這就是依賴注入所要達到的目的。框架

用php實現一個輕量的依賴注入容器

首先咱們建立一個類,看起來是這樣的:運維

<?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容器還很簡陋,還不支持複雜的注入,你能夠繼續完善它。

不過,經過這些代碼你已經瞭解什麼是依賴在注入了,你能夠將這種思想應用到你的項目中,或者着手開發你本身的框架。若是想繼續深刻學習的話,煩請 click Scene

完。

原文地址:https://www.jianshu.com/p/cb0693dd8d2e

相關文章
相關標籤/搜索