答案变得比我最初想要的要长。我提供了一些背景信息。不过,如果您正在寻找短期解释,请阅读 IoC 容器的第一段和粗体段落。
依赖注入
依赖注入是一种设计模式,顾名思义。它将对象注入到其他对象的构造函数或方法中,使得一个对象依赖于一个或多个其他对象.
<?php
class DatabaseWriter {
protected $db;
public function __construct(DatabaseAdapter $db)
{
$this->db = $db;
}
public function write()
{
$this->db->query('...');
}
}
你可以看到我们需要类构造函数aDatabaseAdapter
要传递的实例。由于我们在构造函数中执行此操作,因此如果没有它,则无法实例化该类的对象:我们正在注入一个依赖项。现在,我们知道DatabaseAdapter
始终存在于类中,我们可以轻松依赖它。
The write()
方法只是调用适配器上的方法,因为我们肯定知道它存在,因为我们使用了 DI。
使用 DI 而不是滥用静态类、上帝对象和其他类似的东西的巨大优势是,您可以轻松追踪依赖项的来源.
另一个巨大的优势是,您可以轻松地交换依赖项。如果您想使用依赖项的另一个实现,只需将其传递给构造函数即可。您不再需要寻找硬编码实例来交换它们。
抛开以下事实不谈,通过使用依赖注入,您将能够轻松地对您的类进行单元测试,因为您可以模拟依赖项,而这对于硬编码的依赖项来说是几乎不可能的。
依赖注入的三种类型
构造函数注入
上面解释的依赖注入类型称为构造函数注入。这仅仅意味着依赖项作为参数传递给类构造函数。然后,依赖项将存储为属性,从而可在该类的所有方法中使用。这里的一大优点是,如果不传递依赖关系,类的对象就不能存在。
二传手注射
这种类型使用专用方法来注入依赖项。而不是使用构造函数。使用 Setter Injection 的优点是,您可以向对象添加依赖项创建后。它通常用于可选的依赖项。 Setter 注入也非常适合整理您的构造函数,并且仅在您需要的方法中包含依赖项。
<?php
class RegisterUserService {
protected $logger;
public function setLogger( Logger $logger )
{
$this->logger = $logger;
}
public function registerUser()
{
// Do stuff to register the user
if($this->logger)
$this->logger->log("User has been registered");
}
}
$service = new RegisterUserService;
$service->registerUser(); // Nothing is Logged
$service->setLogger(new ConcreteLogger);
$service->registerUser(); // Now we log
该对象可以在没有任何依赖关系的情况下实例化。有一种方法可以注入依赖项(setLogger()
) 可以选择性地调用。现在由方法实现决定是否使用依赖项(如果未设置)。
值得指出的是,对 Setter 注入要谨慎。调用尚未注入的依赖项上的方法或访问属性将导致令人讨厌的结果Fatal error: Call to a member function XXX() on a non-object
。因此,每次访问依赖项时,都必须首先对其进行空检查。一个更干净的方法来解决这个问题是使用空对象模式 http://en.wikipedia.org/wiki/Null_Object_pattern并将依赖项移至构造函数中(作为可选参数,如果未传递任何内容,则在类内部创建空对象)
接口注入
接口注入的基本思想是,注入依赖项的方法是在接口中定义的。需要依赖的类必须实现该接口。从而确保所需的依赖关系能够正确地注入到依赖对象中。它是之前解释的 Setter 注入的更严格形式。
<?php
interface Database {
public function query();
}
interface InjectDatabaseAccess {
// The user of this interface MUST provide
// a concrete of Database through this method
public function injectDatabase( Database $db );
}
class MySQL implements Database {
public function query($args)
{
// Execute Query
}
}
class DbDoer implements InjectDatabaseAccess {
protected $db;
public function injectDatabase( Database $db )
{
$this->db = $db;
}
public function doSomethingInDb($args)
{
$this->db->query();
}
}
$user = new DbDoer();
$user->injectDatabase( new MySQL );
$user->doSomethingInDb($stuff);
接口注入的意义是有争议的。我个人从未使用过它。我更喜欢构造函数注入而不是它。这使我能够完成完全相同的任务,而无需在注入器端添加额外的接口。
依赖倒置
我们也可以依赖抽象,而不是依赖于具体实例。
<?php
class DatabaseWriter {
protected $db;
public function __construct(DatabaseAdapterInterface $db)
{
$this->db = $db;
}
public function write()
{
$this->db->query('...');
}
}
Now we 反转控制 http://en.wikipedia.org/wiki/Dependency_inversion_principle。我们现在可以注入任何消耗该实例的实例,而不是依赖于具体实例类型提示 http://php.net/manual/en/language.oop5.typehinting.php界面。该接口负责确保后面的具体实例实现我们将要使用的所有方法,以便我们仍然可以在依赖类中依赖它们。
确保你得到了它,因为它是 IoC 容器的核心概念。
国际奥委会容器
我能想到的最简短的术语是这样描述 IoC 容器的:
IoC-Container 是一个知道实例如何创建并知道如何all它们的潜在依赖关系以及如何解决它们。
如果我们以上面的例子为例,想象一下DatabaseAdapter
本身有它自己的依赖关系。
class ConcreteDatabaseAdapter implements DatabaseAdapterInterface{
protected $driver;
public function __construct(DatabaseDriverInterface $driver)
{
$this->driver = $driver;
}
}
所以为了能够使用DatabaseAdapter
你需要传递一个实例DatabaseDriverInterface
抽象作为依赖。
但你的DatabaseWriter
班级不知道,也不应该知道. The DatabaseWriter
不应该关心如何DatabaseAdapter
有效,它应该只关心DatabaseAdapter
是通过的,而不是需要如何创建的。这就是 IoC 容器派上用场的地方。
App::bind('DatabaseWriter', function(){
return new DatabaseWriter(
new ConcreteDatabaseAdapter(new ConcreteDatabaseDriver)
);
});
正如我已经说过的,DatabaseWriter
它本身不知道任何有关它的依赖项的依赖项的信息。但 IoC 容器知道它们的一切,也知道在哪里可以找到它们。所以最终当DatabaseWriter
类将被实例化,首先询问 IoC-Container需要如何实例化。这就是 IoC 容器的作用。
简单地说(如果您喜欢设计模式)。它有点像一个组合依赖注入容器(我上面已经解释过)和服务定位器 http://en.wikipedia.org/wiki/Service_locator_pattern.