依賴注入和容器

前言

依賴注入,也叫控制反轉。簡單的來說就是:一個類中用到其他類的實例時,我們不在該類中創建實例,而是在類外創建實例後把實例作爲參數傳入類中。
當需要創建大量的類的實例的時候,我們爲了方便管理,把類實例化的過程分離出來,並存儲起來統一管理,這就叫 容器

初級實現

我們通過實現一個緩存模塊,來展示依賴注入和容器的基本使用。
首先,建立一個容器類Di.php

<?php   
class Di
{
    protected  $_definitions= [];//存儲依賴實例
    public function set($name, $definition)
    {
        $this->_definitions[$name] = $definition;
    }
    public function get($name)
    {
        if (isset($this->_definitions[$name])) {
            $definition = $this->_definitions[$name];
        } else {
            throw new Exception("class not exist");
        }
        if (is_object($definition) || is_callable($definition)) {
            $instance = call_user_func($definition);
        }else{
            throw new Exception("class not obj");
		}
    }
}

緩存類型有file、Db、Redis,我們建立這三種類型對應的操作類

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

class db 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) { }
}

建立Cache類,即被注入類

class cache
{
    protected $_di;

    protected $_options;

    protected $_connect;

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

    public function setDI($di)
    {
        $this->_di = $di;
         $options = $this->_options;
        if (isset($options['connect'])) {
            $service = $options['connect'];
        } else {
            $service = 'redis';
        }
		//根據參數,從容器中找出對應的實例
        $this->_connect = $this->_di->get($service);
    }


    public function get($key, $lifetime)
    {
        $connect = $this->_connect;
        return $connect->find($key, $lifetime);
    }

    public function save($key, $value, $lifetime)
    {
        $connect = $this->_connect;
        return $connect->save($key, $lifetime);
    }

    public function delete($key)
    {
        $connect = $this->_connect;
        $connect->delete($key, $lifetime);
    }
}
/*****************調用cache************************/
$di = new Di();
//  往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 = $di->get('cache');

高級實現

以上過程實現了簡單的依賴注入和容器,但是我們發現,上面的依賴關係只有一層。如果此時Db類又依賴了其他類時,上面的代碼可能要這麼改

class db implements BackendInterface
{
	public $_options;
	public $_di;
	public __construct($options){
		 if (isset($options['type'])) {
            $service = $options['type'];
        } else {
            $service = 'mysql';
        }
		//根據參數,從容器中找出對應的實例
        return $this->_di->get($service);
	}
	public function setDI($di)
    {
        $this->_di = $di;
    }
    public function find($key, $lifetime) {
		$connect = $this->di->connect();
		return $connect->find();
	 }
    public function save($key, $value, $lifetime) { }
    public function delete($key) { }
    }
}
    /*************************調用***********************************/
    $di = new Di();
//  往Di容器中注入需要用到的實例
$di->set('mysql', function() {
     return new mysql([
         'host' => '127.0.0.1',
         'port' => 6379
     ]);
});
$di->set('db', function() use ($di) {
    $db = new db([
         ‘type’=> 'mysql',
     ]);
     $db->setDi($di);
     return $db;
});


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

上面代碼:緩存使用了db方式即數據庫方式存儲,然後db類型使用的mysql。所以我們先要把mysql類注入到容器,然後再把db類注入到容器,最後把cache注入容器並調用,而且順序必須按照上面的樣子,否則後面的類在注入時找不到依賴的類會報錯。

當依賴層級較多的時候,一個個的注入不僅不方便,一旦順序錯誤也會造成錯誤。這個時候我們就需要使用php的ReflectionClass反射機制,構建一個自動注入且不需要關心注入順序的Di容器。

上面的例子中,我們需要先把依賴的對象注入到Di容器中,在被注入的類中需要用到時從Di容器中取。自動注入則是在被注入函數實例化或者方法被調用時,根據構造函數或方法中指定的參數類型,把對象類型的參數實例化後再傳入被注入類中。而要獲取類的各種信息,就需要用到反射類 ReflectionClass。改造後Di代碼如下:

class Di {
	protected $_definitions=[];
    // 獲得類的對象實例
    public static function getInstance($className) {
		if(isset($this->_definitions[$className]))	return $this->_definitions[$className];
        $paramArr = self::getMethodParams($className);
        $this->_definition[$className] = (new ReflectionClass($className))->newInstanceArgs($paramArr);
        return $this->_definition[$className];
    }

    //直接調用類的方法
    public static function make($className, $methodName, $params = []) {
        $instance = self::getInstance($className);
        // 獲取方法所需要依賴注入的參數
        $paramArr = self::getMethodParams($className, $methodName);
        return $instance->$methodName(array_merge($paramArr, $params));
    }

    // 獲得類的方法參數,把對象類參數實例化
    protected static function getMethodParams($className, $methodsName = '__construct') {

        // 通過反射獲得該類的信息
        $class = new ReflectionClass($className);
        $paramArr = []; // 記錄參數,和參數類型

        // 判斷函數方法名是否存在
        if ($class->hasMethod($methodsName)) {
            $construct = $class->getMethod($methodsName);
            // 判斷函數方法是否有參數
            $params = $construct->getParameters();
            if (count($params) > 0) {
                // 判斷參數類型
                foreach ($params as $key => $param) {
                    if (is_obj($param)) {
                        // 獲得參數類型名稱
                        $paramClassName = get_class($param);
                        //遞歸獲取依賴的對象是否依賴其他對象
                        $args = self::getMethodParams($paramClassName);
                        //返回依賴對象的實例作爲參數
                        $paramArr[] = (new ReflectionClass($paramClassName))->newInstanceArgs($args);
                    }
                }
            }
        }
        return $paramArr;
    }
}

cache和db代碼改造後如下

class cache
{
    protected $_connect;

    public function __construct(db $db)
    {
        $this->_connect= $db;
    }
    public function get($key, $lifetime)
    {
        $connect = $this->_connect;
        return $connect->find($key, $lifetime);
    }

    public function save($key, $value, $lifetime)
    {
        $connect = $this->_connect;
        return $connect->save($key, $lifetime);
    }

    public function delete($key)
    {
        $connect = $this->_connect;
        $connect->delete($key, $lifetime);
    }
}


class db implements BackendInterface
{
	public $_connect;
	public __construct(mysql $mysql){
        $this->_connect = $mysql->connect();
	}
    public function find($key, $lifetime) {
		$connect = $this->_connect();
		return $connect->find();
	 }
    public function save($key, $value, $lifetime) { }
    public function delete($key) { }
    }
}

//調用
$cache = Di::getInstance('cache');

改造後我們無需註冊依賴就可以直接調用實例化cache並調用方法。

但這個這個時候,代碼的侷限性很大,當我們需要調整cache的存儲方式爲redis時,發現不能通過傳入參數調整。代碼還需要優化,主要是Di中自動解析依賴的地方,要支持數組的解析。而且涉及多層依賴多層配置時,還是要引入註冊機制,但此時是的註冊只指定依賴關係,不實例化依賴對象,當Di解析依賴時,可根據註冊的依賴關係獲取依賴對象。且此時註冊也不需要分先後順序。
修改後代碼如下

class Di {
	protected $_dependencies=[];//存儲依賴對象實例
	protected $_definitions=[];//存儲註冊依賴信息
	protected $_params=[];//存儲映射對象的參數
    // 註冊依賴關係
    public function set($className,$param) {
		if(isset($this->_definitions[$className]))	return $this->_definitions[$className];
        $paramArr = self::getMethodParams($className);
        $this->_definition[$className] = (new ReflectionClass($className))->newInstanceArgs($paramArr);
		
       $this->_definitions[$className] = $param['class'];
       $this->_params[$className] = $param;
    }

    // 獲取依賴
    public function get($className) {
		if(isset($this->_dependencies($className)))	return $this->_dependencies($className));
		// 使用PHP的反射機制來獲取類的有關信息,主要就是爲了獲取依賴信息
        $reflection = new ReflectionClass($this->_definitions[$className]);
		$dependencies = unset($this->params[$className]);
        // 通過類的構建函數的參數來了解這個類依賴於哪些單元
        $constructor = $reflection->getConstructor();
		    if ($constructor !== null) {
		        foreach ($constructor->getParameters() as $param) {
		            if ($param->isDefaultValueAvailable()) {
		                // 構造函數如果有默認值,將默認值作爲依賴。即然是默認值了,
		                // 就肯定是簡單類型了。
		                $dependencies[] = $param->getDefaultValue();
		            } else {
		                $c = $param->getClass();
		                // 構造函數沒有默認值,則爲其創建一個引用。
		                // 就是前面提到的 Instance 類型。
		                $dependencies[] = $this->get($c->getName());
		            }
		        }
		    }
			$obj = $reflection->newInstanceArgs($dependencies);
			$this->_dependencies[$className] = $obj;
			return $obj;
    }
}

class cache
{
    protected $_connect;
	//類的依賴信息儘量放在構造函數中,便於在實例化類時解析相關依賴
	//解析依賴時根據指定的實例路徑生成映射,我們從上面Di的get代碼中看到,獲取依賴對象的映射時
	//並不是直接使用的指定路徑,而是通過指定的路徑從_definitions中獲取真實的依賴類信息,這樣我們需要用到不同模塊時,只要修改配置中的class等信息即可
    public function __construct(BackendInterface $cache)
    {
        $this->_connect= $cache;
    }
    public function get($key, $lifetime)
    {
        $connect = $this->_connect;
        return $connect->find($key, $lifetime);
    }

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

    public function delete($key)
    {
    }
}

class db implements BackendInterface
{
	public $connect;
	public __construct(sql $db){
		 $this->connect = $db;
	}
    public function find($key, $lifetime) {
		return $this->connect->find();
	 }
    public function save($key, $value, $lifetime) { }
    public function delete($key) { }
    }
}
//調用
$di = new Di();
//註冊部分先後順序,因爲此時並沒有實例化,只是註冊了依賴關係
$di->set('cache',['class'=>'\\cache'])
$di->set('BackendInterface ',['class'=>'\\app\\db']);//使用數據庫存儲緩存數據
//若我們要用redis方式存儲緩存數據,可以修改後面的配置信息
//如:$di->set('BackendInterface ',['class'=>'\\app\\redis','host'=>'','name'=>'']);

$di->set('sql',['class'=>'\\sql\\mysql','host'=>'127.0.0.1','name'=>'test']);
//調用時自動解析依賴並實例化
$cache = $di->get('cache');
發佈了21 篇原創文章 · 獲贊 3 · 訪問量 9859
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章