一、為什么需要依賴注入
首先我們先不管什么是依賴注入,先來分析一下沒有使用依賴注入會有什么樣的結果。假設我們有一個gmail郵件服務類GMail,然后有另一個類User,User類需要使用發郵件的功能,於是我們在User類中定義一個成員變量$mailServer,並且在聲明這個變量的時候就給它賦值一個GMail類對象,或者在User構造函數中進行GMail類實例化與賦值。這樣寫程序會有什么問題呢?試想一下,每次當我們需要把User使用的郵件服務改為其他類型郵件服務的時候,我們需要頻繁修改User類的成員變量$mailServer,這樣是不好的。問題的根源就在於,我們不該把User類的成員變量$mailServer的實例化寫死在User類內部,而應該在調用User類的時候可以動態決定賦值給$mailServer的對象類型,依賴注入就是來解決這個問題的。
二、依賴注入是什么
所謂依賴注入,實質上就是當某個類對象需要使用另一個類實例的時候,不在類內部實例化另一個類,而將實例化的過程放在類外面實現,實例化完成后再賦值給類對象的某個屬性。這樣的話該類不需要知道賦值給它的屬性的對象具體屬於哪個類的,當需要改變這個屬性的類型的時候,無需對這個類的代碼進行任何改動,只需要在使用該類的地方修改實例化的代碼即可。
依賴注入的方式有兩種:1.構造函數注入,將另一個類的對象作為參數傳遞給當前類的構造函數,在構造函數中給當前類屬性賦值;2.屬性注入,可以將該類某個屬性設置為public屬性,也可以編寫這個屬性的setter方法,這樣就可以在類外面給這個屬性賦值了。
三、依賴注入容器
仔細思考一下,我們會發現,雖然依賴注入解決了可能需要頻繁修改類內部代碼的問題,但是卻帶來了另一個問題。每次我們需要用到某個類對象的時候,我們都需要把這個類依賴的類都實例化,所以我們需要重復寫這些實例化的代碼,而且當依賴的類又依賴於其他類的時候,我們還要找出所有依賴類依賴的其他類然后實例化,可想而知,這是一個繁瑣低效而又麻煩且容易出錯的過程。這個時候依賴注入容器應運而生,它就是來解決這個問題的。
依賴注入容器可以幫我們實例化和配置對象及其所有依賴對象,它會遞歸分析類的依賴關系並實例化所有依賴,而不需要我們去為這個事情費神。
在yii2.0中,yii\di\Container就是依賴注入容器,這里先簡單說一下這個容器的使用。我們可以使用該類的set()方法來注冊一個類的依賴,把依賴信息傳遞給它就可以了,如果希望這個類是單例的,則可以使用setSingleton()方法注冊依賴。注冊依賴之后,當你需要這個類的對象的時候,使用Yii::createObject(),把類的配置參數傳遞過去,yii\di\Container即會幫你解決這個類的所有依賴並創建一個對象返回。
四、yii依賴注入容器 - 依賴注冊
好了,下面開始分析一下yii的依賴注入容器的實現原理。首先來看一下Container的幾個成員變量:
/**
* @var array 存儲單例對象,數組的鍵是對象所屬類的名稱
*/
private $_singletons = [];
/**
* @var array 存儲依賴定義,數組的鍵是對象所屬類的名稱
*/
private $_definitions = [];
/**
* @var array 存儲構造函數參數,數組的鍵是對象所屬類的名稱
*/
private $_params = [];
/**
* @var array 存儲類的反射類對象,數組的鍵是類名或接口名
*/
private $_reflections = [];
/**
* @var array 存儲類的依賴信息,數組的鍵是類名或接口名
*/
private $_dependencies = [];
其中前三個是用於依賴注冊的時候存儲一些類參數和依賴定義的,后兩個則是用於存儲依賴信息的,這樣使用同一個類的時候不用每次都進行依賴解析,直接使用這兩個變量緩存的依賴信息即可。
接下來看看依賴注冊的兩個方法:
/**
* 在DI容器注冊依賴(注冊之后每次請求都將返回一個新的實例)
* @param string $class:類名、接口名或別名
* @param array $definition:類的依賴定義,可以是一個PHP回調函數,一個配置數組或者一個表示類名的字符串
* @param array $params:構造函數的參數列表,在調用DI容器的get()方法獲取類實例的時候將被傳遞給類的構造函數
* @return \yii\di\Container
*/
public function set($class, $definition = [], array $params = [])
{
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);//保存類配置信息
$this->_params[$class] = $params;//保存構造函數參數列表
unset($this->_singletons[$class]);//若存在單例依賴信息則刪除
return $this;
}
/**
* 在DI容器注冊依賴(注冊之后每次請求都將返回同一個實例)
* @param string $class:類名、接口名或別名
* @param array $definition:類的依賴定義,可以是一個PHP回調函數,一個配置數組或者一個表示類名的字符串
* @param array $params:構造函數的參數列表,在調用DI容器的get()方法獲取類實例的時候將被傳遞給類的構造函數
* @return \yii\di\Container
*/
public function setSingleton($class, $definition = [], array $params = [])
{
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
$this->_singletons[$class] = null;//賦值null表示尚未實例化
return $this;
}
這兩個方法很簡單,就是把依賴注冊傳入的參數信息保存下來,提供給實例化過程使用。這兩個方法中都調用了normalizeDefinition()方法,這個方法只是用於規范依賴定義的,源碼如下:
/**
* 規范依賴定義
* @param string $class:類名稱
* @param array $definition:依賴定義
* @return type
* @throws InvalidConfigException
*/
protected function normalizeDefinition($class, $definition)
{
if (empty($definition)) {//若為空,將$class作為類名稱
return ['class' => $class];
} elseif (is_string($definition)) {//若是字符串,默認其為類名稱
return ['class' => $definition];
} elseif (is_callable($definition, true) || is_object($definition)) {//若是PHP回調函數或對象,直接作為依賴的定義
return $definition;
} elseif (is_array($definition)) {//若是數組則需要確保包含了類名稱
if (!isset($definition['class'])) {
if (strpos($class, '\\') !== false) {
$definition['class'] = $class;
} else {
throw new InvalidConfigException("A class definition requires a \"class\" member.");
}
}
return $definition;
} else {
throw new InvalidConfigException("Unsupported definition type for \"$class\": " . gettype($definition));
}
}
五、yii依賴注入容器 - 對象實例化
接下來就是重頭戲了,yii依賴注入容器是怎么根據依賴注冊的信息實現對象實例化的呢?我們一步一步來分析。在第三部分我們說到,當需要創建一個類對象的時候,我們調用的時候Yii::createObject()方法,這個方法里面調用的是Container的get()方法。為了使得講解的思路更清晰,這里我們先來看一下Container的另外兩個方法,getDependencies()和resolveDependencies(),它們分別用於解析類的依賴信息和解決類依賴,會在對象實例化的過程中被調用。
下面先來看看getDependencies()方法:
/**
* 解析指定類的依賴信息(利用PHP的反射機制)
* @param string $class:類名、接口名或別名
* @return type
*/
protected function getDependencies($class)
{
if (isset($this->_reflections[$class])) {//存在該類的依賴信息緩存,直接返回
return [$this->_reflections[$class], $this->_dependencies[$class]];
}
$dependencies = [];
$reflection = new ReflectionClass($class);//創建該類的反射類以獲取該類的信息
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
foreach ($constructor->getParameters() as $param) {//遍歷構造函數參數列表
if ($param->isDefaultValueAvailable()) {//若存在默認值則直接使用默認值
$dependencies[] = $param->getDefaultValue();
} else {//獲取參數類型並創建引用
$c = $param->getClass();
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
//保存該類的反射類對象和依賴信息
$this->_reflections[$class] = $reflection;
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
首先判斷該類是已被解析過,如果是,直接返回緩存中該類的依賴信息,否則,利用PHP的反射機制對類的依賴進行分析,最后將分析所得依賴信息緩存,具體步驟已在代碼中注明。其中Instance類實例用於表示一個指定名稱的類的對象引用,也就是說getDependencies()方法調用之后,得到的$dependencies只是某個類的依賴信息,指明這個類依賴於哪些類,還沒有將這些依賴的類實例化,這個工作是由resolveDependencies()方法來完成的。
再來看看resolveDependencies()方法:
/**
* 解決依賴
* @param array $dependencies:依賴信息
* @param ReflectionClass $reflection:放射類對象
* @return type
* @throws InvalidConfigException
*/
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {//遍歷依賴信息數組,把所有的對象引用都替換為對應類的實例對象
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {//組件id不為null,以id為類名實例化對象
$dependencies[$index] = $this->get($dependency->id);
} elseif ($reflection !== null) {//若id為null但$reflection不為null,通過$reflection獲取構造函數類型,報錯。。
$name = $reflection->getConstructor()->getParameters()[$index]->getName();
$class = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
}
}
}
return $dependencies;
}
這個方法就是遍歷getDependencies()方法得到的關於某個類的依賴信息數組,對每個依賴的類調用Container的get()方法來獲取對象實例化。前面說到get()函數實例化過程中會調用這個方法,而這里又調用了get()方法,所以已經可以知道,依賴解析的過程其實是一個遞歸解析的過程。
再回頭來看看get()方法:
/**
* DI容器返回一個請求類的實例
* @param string $class:類名
* @param array $params:構造函數參數值列表,按照構造函數中參數的順序排列
* @param array $config:用於初始化對象屬性的配置數組
* @return type
* @throws InvalidConfigException
*/
public function get($class, $params = [], $config = [])
{
if (isset($this->_singletons[$class])) {//若存在此類的單例則直接返回單例
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {//若該類未注冊依賴,調用build()函數,使用PHP的反射機制獲取該類的依賴信息,解決依賴,創建類對象返回
return $this->build($class, $params, $config);
}
$definition = $this->_definitions[$class];
if (is_callable($definition, true)) {//若依賴定義是一個PHP函數則直接調用這個函數創建對象
$params = $this->resolveDependencies($this->mergeParams($class, $params));//解決依賴
$object = call_user_func($definition, $this, $params, $config);
} elseif (is_array($definition)) {//若依賴定義為數組,則合並參數,創建對象
$concrete = $definition['class'];
unset($definition['class']);
$config = array_merge($definition, $config);
$params = $this->mergeParams($class, $params);
if ($concrete === $class) {//遞歸解析終止
$object = $this->build($class, $params, $config);
} else {//遞歸進行依賴解析
$object = $this->get($concrete, $params, $config);
}
} elseif (is_object($definition)) {//若依賴定義為對象則保存為單例
return $this->_singletons[$class] = $definition;
} else {
throw new InvalidConfigException('Unexpected object definition type: ' . gettype($definition));
}
if (array_key_exists($class, $this->_singletons)) {//若該類注冊過單例依賴則實例化之
$this->_singletons[$class] = $object;
}
return $object;
}
首先判斷是否存在所需類的單例,若存在則直接返回單例,否則判斷該類是否已注冊依賴,若已注冊依賴,則根據注冊的依賴定義創建對象,具體每一步已在代碼注釋說明。其中mergeParams()方法只是用來合並用戶指定的參數和依賴注冊信息中的參數。若未注冊依賴,則調用build()方法,這個方法又干了些什么呢?看源碼:
/**
* 創建指定類的對象
* @param string $class:類名稱
* @param array $params:構造函數參數列表
* @param array $config:初始化類對象屬性的配置數組
* @return type
* @throws NotInstantiableException
*/
protected function build($class, $params, $config)
{
list ($reflection, $dependencies) = $this->getDependencies($class);//獲取該類的依賴信息
foreach ($params as $index => $param) {//將構造函數參數列表加入該類的依賴信息中
$dependencies[$index] = $param;
}
$dependencies = $this->resolveDependencies($dependencies, $reflection);//解決依賴,實例化所有依賴的對象
if (!$reflection->isInstantiable()) {//類不可實例化
throw new NotInstantiableException($reflection->name);
}
if (empty($config)) {//配置數組為空,使用依賴信息數組創建對象
return $reflection->newInstanceArgs($dependencies);
}
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
$dependencies[count($dependencies) - 1] = $config;//按照 Object 類的要求,構造函數的最后一個參數為 $config 數組
return $reflection->newInstanceArgs($dependencies);
} else {//先使用依賴信息創建對象,再使用$config配置初始化對象屬性
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
首先,由於沒有類的依賴信息,調用getDependencies()方法分析得到依賴信息。然后調用resolveDependencies()方法解決依賴,實例化所有依賴類對象,最后就是創建對象了。