理解依赖注入,laravel IoC容器
在看laravel文档的时候,有一个服务容器(IoC)的概念。它是这样介绍的:Laravel 服务容器是一个用于管理类依赖和执行依赖注入的强大工具。依赖注入听上去很花哨,其实质是通过构造函数或者某些情况下通过「setter」方法将类依赖注入到类中。
但是上面并没有说明原理,是怎么来的呢?
在搜索的时候,看到了Phalcon的中文文档(http://docs.iphalcon.cn/),这份文档写的非常好,其中里面就有一篇《Dependency Injection Explained》,详细解释了依赖注入的原理。
外国人写的文档还是很详细易懂的。
下面的文章来自:http://docs.iphalcon.cn/reference/di-explained.html
接下来的例子有些长,但解释了为什么我们使用依赖注入与服务定位器. 首先,假设我们正在开发一个组件,叫SomeComponent,它执行的内容现在还不重要。 我们的组件需要依赖数据库的连接。
在下面第一个例子中,数据库的连接是在组件内部建立的。这种方法是不实用的;事实上这样做的话,我们不能改变创建数据库连接的参数或者选择不同的数据库系统,因为连接是当组件被创建时建立的。
<?php
class SomeComponent
{
/**
* 连接数据库的实例是被写死在组件的内部
* 因此,我们很难从外部替换或者改变它的行为
*/
public function someDbTask()
{
$connection = new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
// ...
}
}
$some = new SomeComponent();
$some->someDbTask();
为了解决这样的情况,我们建立一个setter,在使用前注入独立外部依赖。现在,看起来似乎是一个不错的解决办法:
<?php
class SomeComponent
{
protected $_connection;
/**
* 设置外部传入的数据库的连接实例
*/
public function setConnection($connection)
{
$this->_connection = $connection;
}
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
}
$some = new SomeComponent();
// 建立数据库连接实例
$connection = new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
// 向组件注入数据连接实例
$some->setConnection($connection);
$some->someDbTask();
想一下,假设我们使用这个组件在应用内的好几个地方都用到,然而我们在注入连接实例时还需要建立好几次数据的连接实例。 如果我们可以获取到数据库的连接实例而不用每次都要创建新的连接实例,使用某种全局注册表可以解决这样的问题:
<?php
class Registry
{
/**
* 返回数据库连接实例
*/
public static function getConnection()
{
return new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
}
}
class SomeComponent
{
protected $_connection;
/**
* 设置外部传入的数据库的连接实例
*/
public function setConnection($connection)
{
$this->_connection = $connection;
}
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
}
$some = new SomeComponent();
// 把注册表中的连接实例传递给组件
$some->setConnection(Registry::getConnection());
$some->someDbTask();
现在,让我们设想一下,我们必须实现2个方法,第一个方法是总是创建一个新的连接,第二方法是总是使用一个共享连接:
<?php
class Registry
{
protected static $_connection;
/**
* 建立一个新的连接实例
*/
protected static function _createConnection()
{
return new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
}
/**
* 只建立一个连接实例,后面的请求只返回该连接实例
*/
public static function getSharedConnection()
{
if (self::$_connection === null) {
self::$_connection = self::_createConnection();
}
return self::$_connection;
}
/**
* 总是返回一个新的连接实例
*/
public static function getNewConnection()
{
return self::_createConnection();
}
}
class SomeComponent
{
protected $_connection;
/**
* 设置外部传入的数据库的连接实例
*/
public function setConnection($connection)
{
$this->_connection = $connection;
}
/**
* 这个方法总是需要共享连接实例
*/
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
/**
* 这个方法总是需要新的连接实例
*/
public function someOtherDbTask($connection)
{
}
}
$some = new SomeComponent();
// 注入共享连接实例
$some->setConnection(
Registry::getSharedConnection()
);
$some->someDbTask();
// 这里我们总是传递一个新的连接实例
$some->someOtherDbTask(
Registry::getNewConnection()
);
到目前为止,我们已经看到依赖注入怎么解决我们的问题了。把依赖作为参数来传递,而不是建立在内部建立它们,这使我们的应用更加容易维护和更加解耦。不管怎么样,长期来说,这种形式的依赖注入有一些缺点。
例如,如果这个组件有很多依赖, 我们需要创建多个参数的setter方法来传递依赖关系,或者建立一个多个参数的构造函数来传递它们,另外在使用组件前还要每次都创建依赖,这让我们的代码像这样不易维护:
<?php
// 创建依赖实例或从注册表中查找
$connection = new Connection();
$session = new Session();
$fileSystem = new FileSystem();
$filter = new Filter();
$selector = new Selector();
// 把实例作为参数传递给构造函数
$some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector);
// ... 或者使用setter
$some->setConnection($connection);
$some->setSession($session);
$some->setFileSystem($fileSystem);
$some->setFilter($filter);
$some->setSelector($selector);
假设我们必须在应用的不同地方使用和创建这些对象。如果当你永远不需要任何依赖实例时,你需要去删掉构造函数的参数,或者去删掉注入的setter。为了解决这样的问题,我们再次回到全局注册表创建组件。不管怎么样,在创建对象之前,它增加了一个新的抽象层:
<?php
class SomeComponent
{
// ...
/**
* Define a factory method to create SomeComponent instances injecting its dependencies
*/
public static function factory()
{
$connection = new Connection();
$session = new Session();
$fileSystem = new FileSystem();
$filter = new Filter();
$selector = new Selector();
return new self($connection, $session, $fileSystem, $filter, $selector);
}
}
瞬间,我们又回到刚刚开始的问题了,我们再次创建依赖实例在组件内部!我们可以继续前进,找出一个每次能奏效的方法去解决这个问题。但似乎一次又一次,我们又回到了不实用的例子中。
一个实用和优雅的解决方法,是为依赖实例提供一个容器。这个容器担任全局的注册表,就像我们刚才看到的那样。使用依赖实例的容器作为一个桥梁来获取依赖实例,使我们能够降低我们的组件的复杂性:
<?php
use Phalcon\Di;
use Phalcon\DiInterface;
class SomeComponent
{
protected $_di;
public function __construct(DiInterface $di)
{
$this->_di = $di;
}
public function someDbTask()
{
// 获得数据库连接实例
// 总是返回一个新的连接
$connection = $this->_di->get("db");
}
public function someOtherDbTask()
{
// 获得共享连接实例
// 每次请求都返回相同的连接实例
$connection = $this->_di->getShared("db");
// 这个方法也需要一个输入过滤的依赖服务
$filter = $this->_di->get("filter");
}
}
$di = new Di();
// 在容器中注册一个db服务
$di->set(
"db",
function () {
return new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
}
);
// 在容器中注册一个filter服务
$di->set(
"filter",
function () {
return new Filter();
}
);
// 在容器中注册一个session服务
$di->set(
"session",
function () {
return new Session();
}
);
// 把传递服务的容器作为唯一参数传递给组件
$some = new SomeComponent($di);
$some->someDbTask();
这个组件现在可以很简单的获取到它所需要的服务,服务采用延迟加载的方式,只有在需要使用的时候才初始化,这也节省了服务器资源。这个组件现在是高度解耦。例如,我们可以替换掉创建连接的方式,它们的行为或它们的任何其他方面,也不会影响该组件。
依赖注入与服务定位器(Dependency Injection/Service Location)
使用Phacon\Di可以整合框架的不同组件。
开发者也可以使用这个组件去注入依赖和管理的应用程序中来自不同类的全局实例。
基本上,这个组件实现了 [控制反转](http://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC) 的模式。使用这种模式,组件的对象不用再使用setter或者构造函数去接受依赖实例,而是使用请求服务的依赖注入。这减少了总的复杂性,因为在组件内,只有一个方法去获取所需的依赖实例
字符串注册
使用字符串注册服务需要一个有效的类名称,它将返回指定的类对象,如果类还没有加载的话,将使用自动加载器实例化对象。这种类型不允许向构造函数指定参数:
<?php
// 返回 new Phalcon\Http\Request(); 对象
$di->set(
"request",
"Phalcon\\Http\\Request"
);
类实例(CLASS INSTANCES)注册
这种类型注册服务需要一个对象。实际上,这个服务不再需要初始化,因为它已经是一个对象,可以说,这不是一个真正的依赖注入,但是如果你想强制总是返回相同的对象/值,使用这种方式还是有用的
<?php
use Phalcon\Http\Request;
// 返回 Phalcon\Http\Request(); 对象
$di->set(
"request",
new Request()
);
闭包与匿名函数(CLOSURES/ANONYMOUS FUNCTIONS)注册
这个方法提供了更加自由的方式去注册依赖,但是如果你想从外部改变实例化的参数而不用改变注册服务的代码,这是很困难的:
<?php
use Phalcon\Db\Adapter\Pdo\Mysql as PdoMysql;
$di->set(
"db",
function () {
return new PdoMysql(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "blog",
]
);
}
);
这些限制是可以克服的,通过传递额外的变量到闭包函数里面:
<?php
use Phalcon\Config;
use Phalcon\Db\Adapter\Pdo\Mysql as PdoMysql;
$config = new Config(
[
"host" => "127.0.0.1",
"username" => "user",
"password" => "pass",
"dbname" => "my_database",
]
);
// 把当前域的$config变量传递给匿名函数使用
$di->set(
"db",
function () use ($config) {
return new PdoMysql(
[
"host" => $config->host,
"username" => $config->username,
"password" => $config->password,
"dbname" => $config->name,
]
);
}
);
You can also access other DI services using the get()
method:
<?php
use Phalcon\Config;
use Phalcon\Db\Adapter\Pdo\Mysql as PdoMysql;
$di->set(
"config",
function () {
return new Config(
[
"host" => "127.0.0.1",
"username" => "user",
"password" => "pass",
"dbname" => "my_database",
]
);
}
);
// Using the 'config' service from the DI
$di->set(
"db",
function () {
$config = $this->get("config");
return new PdoMysql(
[
"host" => $config->host,
"username" => $config->username,
"password" => $config->password,
"dbname" => $config->name,
]
);
}
);
复杂的注册(Complex Registration)
如果要求不用实例化/解析服务,就可以改变定义服务的话,我们需要使用数组的方式去定义服务。使用数组去定义服务可以更加详细:
?php
use Phalcon\Logger\Adapter\File as LoggerFile;
// 通过类名和参数,注册logger服务
$di->set(
"logger",
[
"className" => "Phalcon\\Logger\\Adapter\\File",
"arguments" => [
[
"type" => "parameter",
"value" => "../apps/logs/error.log",
]
]
]
);
// 使用匿名函数的方式
$di->set(
"logger",
function () {
return new LoggerFile("../apps/logs/error.log");
}
);
上面两种注册服务的方式的结果是一样的。然而,使用数组定义的话,在需要的时候可以变更注册服务的参数:
<?php
// 改变logger服务的类名
$di->getService("logger")->setClassName("MyCustomLogger");
// 不用实例化就可以改变第一个参数值
$di->getService("logger")->setParameter(
0,
[
"type" => "parameter",
"value" => "../apps/logs/error.log",
]
);
除了使用数组的语法注册服务,你还可以使用以下三种类型的依赖注入:
构造函数注入(CONSTRUCTOR INJECTION)
这个注入方式是通过传递依赖/参数到类的构造函数。让我们假设我们有下面的组件:
<?php
namespace SomeApp;
use Phalcon\Http\Response;
class SomeComponent
{
/**
* @var Response
*/
protected $_response;
protected $_someFlag;
public function __construct(Response $response, $someFlag)
{
$this->_response = $response;
$this->_someFlag = $someFlag;
}
}
这个服务可以这样被注入:
<?php
$di->set(
"response",
[
"className" => "Phalcon\\Http\\Response"
]
);
$di->set(
"someComponent",
[
"className" => "SomeApp\\SomeComponent",
"arguments" => [
[
"type" => "service",
"name" => "response",
],
[
"type" => "parameter",
"value" => true,
],
]
]
);
reponse服务(Phalcon\Http\Response)作为第一个参数传递给构造函数,与此同时,一个布尔类型的值(true)作为第二个参数传递。
设值注入(SETTER INJECTION)
类中可能有setter去注入可选的依赖,前面那个class可以修改成通过setter来注入依赖的方式:
?php
namespace SomeApp;
use Phalcon\Http\Response;
class SomeComponent
{
/**
* @var Response
*/
protected $_response;
protected $_someFlag;
public function setResponse(Response $response)
{
$this->_response = $response;
}
public function setFlag($someFlag)
{
$this->_someFlag = $someFlag;
}
}
用setter方式来注入的服务可以通过下面的方式来注册:
<?php
$di->set(
"response",
[
"className" => "Phalcon\\Http\\Response",
]
);
$di->set(
"someComponent",
[
"className" => "SomeApp\\SomeComponent",
"calls" => [
[
"method" => "setResponse",
"arguments" => [
[
"type" => "service",
"name" => "response",
]
]
],
[
"method" => "setFlag",
"arguments" => [
[
"type" => "parameter",
"value" => true,
]
]
]
]
]
);
属性注入(PROPERTIES INJECTION)
这是一个不太常用的方式,这种方式的注入是通过类的public属性来注入:
<?php
namespace SomeApp;
use Phalcon\Http\Response;
class SomeComponent
{
/**
* @var Response
*/
public $response;
public $someFlag;
}
通过属性注入的服务,可以像下面这样注册:
<?php
$di->set(
"response",
[
"className" => "Phalcon\\Http\\Response",
]
);
$di->set(
"someComponent",
[
"className" => "SomeApp\\SomeComponent",
"properties" => [
[
"name" => "response",
"value" => [
"type" => "service",
"name" => "response",
],
],
[
"name" => "someFlag",
"value" => [
"type" => "parameter",
"value" => true,
],
]
]
]
);
Array Syntax 数组注册
使用数组的方式去注册服务也是可以的:
?php
use Phalcon\Di;
use Phalcon\Http\Request;
// 创建一个依赖注入容器
$di = new Di();
// 通过类名称设置服务
$di["request"] = "Phalcon\\Http\\Request";
// 使用匿名函数去设置服务,这个实例将被延迟加载
$di["request"] = function () {
return new Request();
};
// 直接注册一个实例
$di["request"] = new Request();
// 使用数组方式定义服务
$di["request"] = [
"className" => "Phalcon\\Http\\Request",
];
在上面的例子中,当框架需要访问request服务的内容,它会在容器里面查找名为‘request’的服务。 在容器中将返回所需要的服务的实例。当有需要时,开发者可能最终需要替换这个组件。
每个方法(在上面的例子证明)用于设置/注册服务方面具都具有优势和劣势。这是由开发者和特别的要求决定具体使用哪个。
通过字符串设置一个服务是很简单,但是缺乏灵活性。通过数组设置服务提供了更加灵活的方式,但是使代码更复杂。匿名函数是上述两者之间的一个很好的平衡,但是会导致比预期的更多维护。
Phalcon\Di 对每个储存的服务提供了延迟加载。除非开发者选择直接实例化一个对象并将其存储在容器中,任何储存在里面的对象(通过数组,字符串等等设置的)都将延迟加载,即只要当使用到时才实例化。
从容器中获取服务
从容器中获取一个服务是一件简单的事情,只要通过“get”方法就可以。这将返回一个服务的新实例:
<?php $request = $di->get("request");
或者通过魔术方法的方式获取:
<?php $request = $di->getRequest();
或者通过访问数组的方式获取:
<?php $request = $di["request"];
参数可以传递到构造函数中,通过添加一个数组的参数到get方法中:
<?php // 将返回:new MyComponent("some-parameter", "other") $component = $di->get( "MyComponent", [ "some-parameter", "other", ] );