PHP程式設計師如何理解依賴注入容器(dependency injection container)
背景知識
傳統的思路是應用程式用到一個Foo類,就會建立Foo類並呼叫Foo類的方法,假如這個方法內需要一個Bar類,就會建立Bar類並呼叫Bar類的方法,而這個方法內需要一個Bim類,就會建立Bim類,接著做些其它工作。
// 程式碼【1】
class Bim
{
public function doSomething()
{
echo __METHOD__, '|';
}
}
class Bar
{
public function doSomething ()
{
$bim = new Bim();
$bim->doSomething();
echo __METHOD__, '|';
}
}
class Foo
{
public function doSomething()
{
$bar = new Bar();
$bar->doSomething();
echo __METHOD__;
}
}
$foo = new Foo();
$foo->doSomething(); //Bim::doSomething|Bar::doSomething|Foo::doSomething
使用依賴注入的思路是應用程式用到Foo類,Foo類需要Bar類,Bar類需要Bim類,那麼先建立Bim類,再建立Bar類並把Bim注入,再建立Foo類,並把Bar類注入,再呼叫Foo方法,Foo呼叫Bar方法,接著做些其它工作。
// 程式碼【2】
class Bim
{
public function doSomething()
{
echo __METHOD__, '|';
}
}
class Bar
{
private $bim;
public function __construct(Bim $bim)
{
$this->bim = $bim;
}
public function doSomething()
{
$this->bim->doSomething();
echo __METHOD__, '|';
}
}
class Foo
{
private $bar;
public function __construct(Bar $bar)
{
$this->bar = $bar;
}
public function doSomething()
{
$this->bar->doSomething();
echo __METHOD__;
}
}
$foo = new Foo(new Bar(new Bim()));
$foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething
這就是控制反轉模式。依賴關係的控制反轉到呼叫鏈的起點。這樣你可以完全控制依賴關係,通過調整不同的注入物件,來控制程式的行為。例如Foo類用到了memcache,可以在不修改Foo類程式碼的情況下,改用redis。
使用依賴注入容器後的思路是應用程式需要到Foo類,就從容器內取得Foo類,容器建立Bim類,再建立Bar類並把Bim注入,再建立Foo類,並把Bar注入,應用程式呼叫Foo方法,Foo呼叫Bar方法,接著做些其它工作.
總之容器負責例項化,注入依賴,處理依賴關係等工作。
程式碼演示 依賴注入容器 (dependency injection container)
通過一個最簡單的容器類來解釋一下,這段程式碼來自 Twittee
class Container
{
private $s = array();
function __set($k, $c)
{
$this->s[$k] = $c;
}
function __get($k)
{
return $this->s[$k]($this);
}
}
這段程式碼使用了魔術方法,在給不可訪問屬性賦值時,__set() 會被呼叫。讀取不可訪問屬性的值時,__get() 會被呼叫。
$c = new Container();
$c->bim = function () {
return new Bim();
};
$c->bar = function ($c) {
return new Bar($c->bim);
};
$c->foo = function ($c) {
return new Foo($c->bar);
};
// 從容器中取得Foo
$foo = $c->foo;
$foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething
這段程式碼使用了匿名函式
class IoC
{
protected static $registry = [];
public static function bind($name, Callable $resolver)
{
static::$registry[$name] = $resolver;
}
public static function make($name)
{
if (isset(static::$registry[$name])) {
$resolver = static::$registry[$name];
return $resolver();
}
throw new Exception('Alias does not exist in the IoC registry.');
}
}
IoC::bind('bim', function () {
return new Bim();
});
IoC::bind('bar', function () {
return new Bar(IoC::make('bim'));
});
IoC::bind('foo', function () {
return new Foo(IoC::make('bar'));
});
// 從容器中取得Foo
$foo = IoC::make('foo');
$foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething
依賴注入容器 (dependency injection container) 高階功能
真實的dependency injection container會提供更多的特性,如
-
自動繫結(Autowiring)或 自動解析(Automatic Resolution)
-
註釋解析器(Annotations)
-
延遲注入(Lazy injection)
下面的程式碼在Twittee的基礎上,實現了Autowiring。
class Bim
{
public function doSomething()
{
echo __METHOD__, '|';
}
}
class Bar
{
private $bim;
public function __construct(Bim $bim)
{
$this->bim = $bim;
}
public function doSomething()
{
$this->bim->doSomething();
echo __METHOD__, '|';
}
}
class Foo
{
private $bar;
public function __construct(Bar $bar)
{
$this->bar = $bar;
}
public function doSomething()
{
$this->bar->doSomething();
echo __METHOD__;
}
}
class Container
{
private $s = array();
public function __set($k, $c)
{
$this->s[$k] = $c;
}
public function __get($k)
{
// return $this->s[$k]($this);
return $this->build($this->s[$k]);
}
/**
* 自動繫結(Autowiring)自動解析(Automatic Resolution)
*
* @param string $className
* @return object
* @throws Exception
*/
public function build($className)
{
// 如果是匿名函式(Anonymous functions),也叫閉包函式(closures)
if ($className instanceof Closure) {
// 執行閉包函式,並將結果
return $className($this);
}
/** @var ReflectionClass $reflector */
$reflector = new ReflectionClass($className);
// 檢查類是否可例項化, 排除抽象類abstract和物件介面interface
if (!$reflector->isInstantiable()) {
throw new Exception("Can't instantiate this.");
}
/** @var ReflectionMethod $constructor 獲取類的建構函式 */
$constructor = $reflector->getConstructor();
// 若無建構函式,直接例項化並返回
if (is_null($constructor)) {
return new $className;
}
// 取建構函式引數,通過 ReflectionParameter 陣列返回引數列表
$parameters = $constructor->getParameters();
// 遞迴解析建構函式的引數
$dependencies = $this->getDependencies($parameters);
// 建立一個類的新例項,給出的引數將傳遞到類的建構函式。
return $reflector->newInstanceArgs($dependencies);
}
/**
* @param array $parameters
* @return array
* @throws Exception
*/
public function getDependencies($parameters)
{
$dependencies = [];
/** @var ReflectionParameter $parameter */
foreach ($parameters as $parameter) {
/** @var ReflectionClass $dependency */
$dependency = $parameter->getClass();
if (is_null($dependency)) {
// 是變數,有預設值則設定預設值
$dependencies[] = $this->resolveNonClass($parameter);
} else {
// 是一個類,遞迴解析
$dependencies[] = $this->build($dependency->name);
}
}
return $dependencies;
}
/**
* @param ReflectionParameter $parameter
* @return mixed
* @throws Exception
*/
public function resolveNonClass($parameter)
{
// 有預設值則返回預設值
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
throw new Exception('I have no idea what to do here.');
}
}
// ----
$c = new Container();
$c->bar = 'Bar';
$c->foo = function ($c) {
return new Foo($c->bar);
};
// 從容器中取得Foo
$foo = $c->foo;
$foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething
// ----
$di = new Container();
$di->foo = 'Foo';
/** @var Foo $foo */
$foo = $di->foo;
var_dump($foo);
/*
Foo#10 (1) {
private $bar =>
class Bar#14 (1) {
private $bim =>
class Bim#16 (0) {
}
}
}
*/
$foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething
以上程式碼的原理參考PHP官方文件:反射,PHP 5 具有完整的反射 API,添加了對類、介面、函式、方法和擴充套件進行反向工程的能力。 此外,反射 API 提供了方法來取出函式、類和方法中的文件註釋。
一些複雜的容器會有許多特性,下面列出一些相關的github專案,歡迎補充。