我将尝试总结一些你应该知道的内容,并希望能够解决你的一些困惑。让我们从一个基本的例子开始:
class MySQLAdapter
{
public function __construct()
{
$this->pdo = new PDO();
}
}
class Logger
{
public function __construct()
{
$this->adapter = new MySqlAdapter();
}
}
$log = new Logger();
作为您所看到的,我们正在实例化
Logger
,它有两个依赖项:
MySQLAdapter
和PDO。
这个过程是这样工作的:
- 我们创建了Logger
- Logger创建MySQLAdapter
- MySQLAdapter创建PDO
以上代码可以运行,但如果明天我们决定将数据记录在文件中而不是数据库中,我们将需要更改
Logger
类,并用全新的
FileAdapter
替换
MySQLAdapter
。
class Logger
{
public function __construct()
{
$this->adapter = new FileAdapter();
}
}
这是依赖注入试图解决的问题:
不要因为依赖项发生变化而修改类。
依赖注入
依赖注入是指通过给一个类的构造函数提供所有必需的依赖项来实例化该类的过程。如果我们将依赖注入应用于之前的示例,它将如下所示:
interface AdapterInterface
{
}
class FileAdapter implements AdapterInterface
{
public function __construct()
{
}
}
class MySQLAdapter implements AdapterInterface
{
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
}
class Logger
{
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
}
$log = new Logger(
new MySQLAdapter(
new PDO()
)
);
正如您所看到的,我们在构造函数中没有实例化任何东西,而是将已实例化的类传递给构造函数。这使我们能够替换任何依赖项而不修改该类:
// log to file
$log = new Logger(
new FileAdapter()
);
这有助于我们:
To easily maintain the code:
As you already saw, we don't need to modify the class if one of its dependencies changed.
Makes the code more testable:
When you run your test suite against MySQLAdapter
you don't want to hit the database on each test, so the PDO object will be mocked in tests:
// test snippet
$log = new Logger(
new MySQLAdapter(
$this->getMockClass('PDO', [...])
)
);
问:Logger
如何知道你给他的是它需要的类而不是垃圾?
答:这是接口(AdapterInterface)的工作,它是Logger和其他类之间的合同。Logger“知道”任何实现该特定接口的类都将包含它需要执行其工作的方法。
依赖注入容器:
您可以将此类(即:容器)视为存储运行应用程序所需的所有对象的中心位置。当您需要其中一个对象时,
您从容器中请求对象而不是自己实例化。
您可以将DiC看作是一只被训练出门、取报纸并把它带回给您的狗。问题在于,这只狗只受过前门开着的训练。
只要狗的依赖关系不改变(即门开着),一切都会很好。如果有一天前门关闭了,狗就不知道该如何取报纸了。
但是,如果这只狗有一个IoC容器,它就能找到方法...
控制反转
到目前为止,"经典"代码的初始化过程如下:
IoC只是简单地复制了上述过程,但是顺序相反:
- 创建PDO
- 创建MySQLAdapter并将其提供给PDO
- 创建Logger并将其提供给MySQLAdapter
如果您认为依赖注入是某种IoC,那么您是正确的。当我们谈论依赖注入时,我们有以下示例:
// log to mysql
$log = new Logger(
new MySQLAdapter(
new PDO()
)
);
乍一看,有人可能会认为实例化过程是:
- 创建Logger
- 创建MySQLAdapter
- 创建PDO
事实上,代码将从中间向左解释。因此顺序将是:
IoC容器只是自动化了这个过程。当您从容器请求Logger
时,它使用PHP Reflection和type hinting分析其依赖项(来自构造函数),实例化所有依赖项,将它们发送到请求的类并返回Logger
实例。
注意: 为了查找类的依赖关系,一些IoC容器使用注释而不是类型提示或两者的组合。
因此,回答您的问题:
- 如果容器能够自行解析依赖项,则只需要在应用程序的引导过程中实例化容器即可。(请参见控制反转容器)
- 如果容器无法自行解析依赖项,则需要手动提供运行应用程序所需的对象。这种提供通常发生在引导过程中。(请参见依赖注入容器)
如果您的容器可以自行解析依赖项,但由于各种原因您还需要手动添加更多依赖项,则应在初始化容器后的引导过程中执行此操作。
注意:在实际应用中这两个原则之间会有各种混合,但我试图向您解释它们背后的主要思想。
您的容器将如何呈现仅取决于您,不要害怕重新发明轮子,只要为教育目的而这样做。