在上一篇中我们通过一个具体的Web案例来说明依赖注入,今天我们将谈到依赖注入的容器(Container),首先让我们从一个重要的声明开始:
大多数时侯,你在使用依赖注入方式解耦组件时,并不需要用到容器。
但是如果你要管理很多不同的对像,并且要处理复杂繁多的对像间的依赖关系时,容器就变得很有用了。
还记得第一篇中的例子吗,在创建User对像前首先要创建一个SessionStorage对像。这没什么大不了,但我还是想说,这是因为你在创建你所需要的对像前,你清楚的知道它所依赖的对像了。如果对像很多,依赖关系很复杂(假设SessionStorage类依赖cache类,cache类依赖file类和inputFilter类,file类依赖stdio类),那就呵呵了。。。。
$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);
在后面的文章中我们将介绍Symfony 2中实现容器的方式。不过现在为了能简单、清晰的说明容器,我们先无视Symfony。下面将采用 Zend Framework中的一个例子来说明:
Zend Framework 的Zend_Mail类简化了email管理,它默认使用PHP的mail()函数来发送邮件,但是灵活性欠佳。不过谢天谢地,可以通过提供transport类来轻松改变这些行为。
下面的代码展示如何创建Zend_Mail类并使用Gmail帐号来发送邮件
$transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => 'foo',
'password' => 'bar',
'ssl' => 'ssl',
'port' => 465,
));
$mailer = new Zend_Mail();
$mailer->setDefaultTransport($transport);
依赖注入容器(Dependency Injection Container)是一个大类,它能实例化并配置他所管理的各种组件和类。为了能做到这些,它必须知道这些类的构造方法的参数,依赖关系。
下面是一个硬编码的容器,还是实现之前所提到的获取Zend_Mail对象的工作:
class Container
{
public function getMailTransport()
{
return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => 'foo',
'password' => 'bar',
'ssl' => 'ssl',
'port' => 465,
));
}
public function getMailer()
{
$mailer = new Zend_Mail();
$mailer->setDefaultTransport($this->getMailTransport());
return $mailer;
}
}
//容器的使用也很简单
$container = new Container();
$mailer = $container->getMailer();
在使用容器的时侯,如果需要获得一个Zend_Mail对像,并不需要知道创建它的细节,因为所有创建对像实例的细节都内置到容器中实现了。Zend_Mail对Mail_Transport类的依赖也能通过容器自动注入到Zend_Mail对像中。
获取依赖对像主要是getMailTransport()实现的,容器的强大之处就是靠这个简单的get调用实现的。
但是聪明的你一定发现了问题,容器里面有硬编码(如,发送邮件的帐号密码信息等)。所以我们要更进一步,为容器添加参数,让容器变得更有用。
class Container
{
protected $parameters = array();
public function __construct(array $parameters = array())
{
$this->parameters = $parameters;
}
public function getMailTransport()
{
return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => $this->parameters['mailer.username'],
'password' => $this->parameters['mailer.password'],
'ssl' => 'ssl',
'port' => 465,
));
}
public function getMailer()
{
$mailer = new Zend_Mail();
$mailer->setDefaultTransport($this->getMailTransport());
return $mailer;
}
}
现在通过容器构造函数的参数可以很容易切换发送邮件的帐号和密码
$container = new Container(array(
'mailer.username' => 'foo',
'mailer.password' => 'bar',
));
$mailer = $container->getMailer();
如果觉得Zend_Mail类不能满足当前需要(比如测试时,需要做一些日志),想轻易切换邮件发送类,同样可以通过容器构造函数的参数传递类名
class Container
{
// ...
public function getMailer()
{
$class = $this->parameters['mailer.class'];
$mailer = new $class();
$mailer->setDefaultTransport($this->getMailTransport());
return $mailer;
}
}
$container = new Container(array(
'mailer.username' => 'foo',
'mailer.password' => 'bar',
'mailer.class' => 'MyTest_Mail',
));
$mailer = $container->getMailer();
最后,考虑到客户获取mailer对像时并不需要每次都重新实例化一下(占用开销),容器应该每次提供相同的对像实例。
因此,程序使用了protected的静态数组$shared,用来存储第一次实例化的对像,以后用户获取mailer时都返回第一次实例化的对像
class Container
{
static protected $shared = array();
// ...
public function getMailer()
{
if (isset(self::$shared['mailer']))
{
return self::$shared['mailer'];
}
$class = $this->parameters['mailer.class'];
$mailer = new $class();
$mailer->setDefaultTransport($this->getMailTransport());
return self::$shared['mailer'] = $mailer;
}
}
容器封装了这些基本功能,容器需要管理的内容包括对像的实例化和配置。这些对像本身并不知道自己被容器管理,也可以无视容器的存在。这也就是为什么容器可以管理任何PHP类。如果对像本身在对依赖关系的处理使用了依赖注入这种方式就更好了,当然这不是必须的。
不过手工创建和维护容器很快会变成噩梦。后面的文章讲会讲述Symfony 2是怎么实现容器的。