手動實現 DI 容器(PHP版)
關於依賴注入相信大家應該都經常接觸或者至少有所耳聞,比較知名的框架都支援依賴注入,比如Java的Spring,PHP的Laravel、Symfony等。現在我開始手動實現一個簡陋的DI容器吧。
由開車開始
先開個車,為大家舉個栗子:
class Driver
{
public function drive()
{
$car = new Car();
echo '老司機正在駕駛', $car->getCar(), PHP_EOL;
}
}
class Car
{
protected $name = '普通汽車';
public function getCar()
{
return $this->name;
}
}
有兩個類,Driver和Car,老司機Driver有個方法driver,在呼叫的時候首先得整輛車$car,然後發車。大多數同學都寫過這樣或者類似的程式碼,這樣的程式碼單看沒啥毛病,挺正常的。但是,如果我要換輛車,開普通車撩不到妹。
class Benz extends Car
{
protected $name = '賓士';
}
這時候就需要做一個比較噁心的操作了,得改老司機的程式碼了。(老司機:我做錯了什麼?換輛車還得讓我重學駕照……)。因此我們需要把讓Car為外界注入,將Driver和Car解耦,不是老司機自己開車的時候還得自己去造車。於是就有了下面的結果
class Driver
{
protected $car;
public function __construct(Car $car)
{
$this->car = $car;
}
public function drive()
{
echo '老司機正在駕駛', $this->car->getCar(), PHP_EOL;
}
}
此時Driver和Car兩個類已經解耦,這兩個類的依賴,依靠上層程式碼去管理。此時,老司機會這樣“開車”:
$car = new Car();
$driver = new Driver($car);
$driver->drive();
此時,我們建立Driver依賴的例項,並注入。上面的例子,我們實現了依賴注入,不過是手動的,寫起來感覺還是不爽。這麼繁重的活怎麼能手動來做呢,得讓程式自己去做。於是乎,DI容器誕生。
依賴注入容器
依賴注入與IoC模式類似工廠模式,是一種解決呼叫者和被呼叫者依賴耦合關係的模式。它解決了物件之間的依賴關係,使得物件只依賴IoC/DI容器,不再直接相互依賴,實現鬆耦合,然後在物件建立時,由IoC/DI容器將其依賴(Dependency)的物件注入(Inject)其內,這樣做可以最大程度實現鬆耦合。依賴注入說白一點,就是容器將某個類依賴的其他類的例項注入到這個類的例項中。
這段話可能說的有點抽象,回到剛才的例子吧。剛剛我手動完成了依賴注入,比較麻煩,如果一個大型的專案這樣做肯定會覺得很繁瑣,而且不夠優雅。因此我們需要有一位總管代替我們去幹這個,這個總管就是容器。類的依賴管理全部交給容器去完成。因此,一般來說容器是一個全域性的物件,大家共有的。
做一個自己的DI容器
寫一個功能,我們首先需要分析問題,因此我們先要明白,對於一個簡單的DI容器需要哪些功能,這直接關係到我們程式碼的編寫。對於一個簡單的容器,至少需要滿足以下幾點:
- 建立所需類的例項
- 完成依賴管理(DI)
- 可以獲取單例的例項
- 全域性唯一
綜上,我們的容器類大約長這樣:
class Container
{
/**
* 單例
* @var Container
*/
protected static $instance;
/**
* 容器所管理的例項
* @var array
*/
protected $instances = [];
private function __construct(){}
private function __clone(){}
/**
* 獲取單例的例項
* @param string $class
* @param array ...$params
* @return object
*/
public function singleton($class, ...$params)
{}
/**
* 獲取例項(每次都會建立一個新的)
* @param string $class
* @param array ...$params
* @return object
*/
public function get($class, ...$params)
{}
/**
* 工廠方法,建立例項,並完成依賴注入
* @param string $class
* @param array $params
* @return object
*/
protected function make($class, $params = [])
{}
/**
* @return Container
*/
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
}
大體骨架已經確定,接下來進入最核心的make方法:
protected function make($class, $params = [])
{
//如果不是反射類根據類名建立
$class = is_string($class) ? new ReflectionClass($class) : $class;
//如果傳的入參不為空,則根據入參建立例項
if (!empty($params)) {
return $class->newInstanceArgs($params);
}
//獲取構造方法
$constructor = $class->getConstructor();
//獲取構造方法引數
$parameterClasses = $constructor ? $constructor->getParameters() : [];
if (empty($parameterClasses)) {
//如果構造方法沒有入參,直接建立
return $class->newInstance();
} else {
//如果構造方法有入參,迭代並遞迴建立依賴類例項
foreach ($parameterClasses as $parameterClass) {
$paramClass = $parameterClass->getClass();
$params[] = $this->make($paramClass);
}
//最後根據建立的引數建立例項,完成依賴的注入
return $class->newInstanceArgs($params);
}
}
為了容器的易用,我做了一些完善:
- 實現ArrayAccess介面,使單例例項可以直接通過array的方式獲取,如果該例項沒有,則建立
- 重寫__get方法,更方便的獲取
最終版:
class Container implements ArrayAccess
{
/**
* 單例
* @var Container
*/
protected static $instance;
/**
* 容器所管理的例項
* @var array
*/
protected $instances = [];
private function __construct(){}
private function __clone(){}
/**
* 獲取單例的例項
* @param string $class
* @param array ...$params
* @return object
*/
public function singleton($class, ...$params)
{
if (isset($this->instances[$class])) {
return $this->instances[$class];
} else {
$this->instances[$class] = $this->make($class, $params);
}
return $this->instances[$class];
}
/**
* 獲取例項(每次都會建立一個新的)
* @param string $class
* @param array ...$params
* @return object
*/
public function get($class, ...$params)
{
return $this->make($class, $params);
}
/**
* 工廠方法,建立例項,並完成依賴注入
* @param string $class
* @param array $params
* @return object
*/
protected function make($class, $params = [])
{
//如果不是反射類根據類名建立
$class = is_string($class) ? new ReflectionClass($class) : $class;
//如果傳的入參不為空,則根據入參建立例項
if (!empty($params)) {
return $class->newInstanceArgs($params);
}
//獲取構造方法
$constructor = $class->getConstructor();
//獲取構造方法引數
$parameterClasses = $constructor ? $constructor->getParameters() : [];
if (empty($parameterClasses)) {
//如果構造方法沒有入參,直接建立
return $class->newInstance();
} else {
//如果構造方法有入參,迭代並遞迴建立依賴類例項
foreach ($parameterClasses as $parameterClass) {
$paramClass = $parameterClass->getClass();
$params[] = $this->make($paramClass);
}
//最後根據建立的引數建立例項,完成依賴的注入
return $class->newInstanceArgs($params);
}
}
/**
* @return Container
*/
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
public function __get($class)
{
if (!isset($this->instances[$class])) {
$this->instances[$class] = $this->make($class);
}
return $this->instances[$class];
}
public function offsetExists($offset)
{
return isset($this->instances[$offset]);
}
public function offsetGet($offset)
{
if (!isset($this->instances[$offset])) {
$this->instances[$offset] = $this->make($offset);
}
return $this->instances[$offset];
}
public function offsetSet($offset, $value)
{
}
public function offsetUnset($offset) {
unset($this->instances[$offset]);
}
}
現在藉助容器我們寫一下上面的程式碼:
$driver = $app->get(Driver::class);
$driver->drive();
//output:老司機正在駕駛普通汽車
就這麼簡單,老司機就能發車。這裡預設注入的是Car的例項,如果需要開賓士,那隻需要這樣:
$benz = $app->get(Benz::class);
$driver = $app->get(Driver::class, $benz);
$driver->drive();
//output:老司機正在駕駛賓士
按照PSR-11的要求,依賴注入容器需要實現Psr\Container\ContainerInterface介面,這裡只是演示並未去實現,因為那需要引入Psr依賴庫,比較麻煩,其實也很簡單,只是多了幾個方法,有興趣的可以自己去了解下PSR-11的要求(傳送門)。
這裡只是實現了一個非常簡陋的DI容器,實際中還需要考慮很多,而且這裡的容器功能上還很簡陋。還有一些坑沒處理,比如出現迴圈依賴怎麼處理、延遲載入的機制……
這裡只是我週末閒暇練手的一點記錄,大家如果有興趣可以閱讀以下Laravel或者Symfony容器那塊的原始碼,或者瞭解一下Spring的容器。後續有空我也會繼續完善。