Lou*_*ieV 6 php design-patterns dependency-injection inversion-of-control
出于显而易见的原因,我刚刚开始使用依赖注入,并且没有阅读控制反转 (IoC),很快就发现实例化我的一些类时过于冗长的问题。因此,在阅读 IoC 时,我有一个尚未找到具体答案的问题。什么时候应该进行班级注册?在引导程序中?执行前?如何强制执行依赖项的类型?
我没有使用任何框架。为了学习,我编写了自己的容器。
这是我的容器和一些示例类的一个非常低调的示例。
class DepContainer
{
private static $registry = array();
public static function register($name, Closure $resolve)
{
self::$registry[$name] = $resolve;
}
public static function resolve($name)
{
if (self::registered($name)) {
$name = static::$registry[$name];
return $name();
}
throw new Exception('Nothing bro.');
}
public static function registered($name)
{
return array_key_exists($name, self::$registry);
}
}
class Bar
{
private $hello = 'hello world';
public function __construct()
{
# code...
}
public function out()
{
echo $this->hello . "\n";
}
}
class Foo
{
private $bar;
public function __construct()
{
$this->bar = DepContainer::resolve('Bar');
}
public function say()
{
$this->bar->out();
}
}
Run Code Online (Sandbox Code Playgroud)
这些已经在应用程序结构中。依赖注入方式我会输入提示传入的参数,但没有它我可以做:
DepContainer::register('Bar', function(){
return new Bar();
});
$f = new Foo();
$f->say();
Run Code Online (Sandbox Code Playgroud)
对我来说,在引导程序中注册所有依赖项是有意义的,这将是 IMO 更干净的方式。在运行时就像向你展示的那样我认为和做一样丑陋new Foo(new Bar(...)...)。
Ale*_*chi 13
我将尝试总结一些你应该知道的事情,并且(希望)能够澄清你的一些困境。
让我们从一个基本的例子开始:
class MySQLAdapter
{
public function __construct()
{
$this->pdo = new PDO();
}
}
class Logger
{
public function __construct()
{
$this->adapter = new MySqlAdapter();
}
}
$log = new Logger();
Run Code Online (Sandbox Code Playgroud)
如您所见,我们正在实例化Logger它有两个依赖项:MySQLAdapter和 PDO。
这个过程是这样工作的:
上面的代码有效,但如果明天我们决定需要将数据记录在文件而不是数据库中,我们将需要更改Logger类并替换MySQLAdapter为全新的FileAdapter.
// not good
class Logger
{
public function __construct()
{
$this->adapter = new FileAdapter();
}
}
Run Code Online (Sandbox Code Playgroud)
这就是依赖注入试图解决的问题:不要因为依赖已经改变而修改类。
通过为类的构造函数提供正常运行所需的所有依赖项,可以帮助实例化类的过程。如果我们将依赖注入应用到我们前面的例子中,它看起来像这样:
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 to mysql
$log = new Logger(
new MySQLAdapter(
new PDO()
)
);
Run Code Online (Sandbox Code Playgroud)
如您所见,我们没有在构造函数中实例化任何东西,而是将实例化的类传递给构造函数。这允许我们在不修改类的情况下替换任何依赖项:
// log to file
$log = new Logger(
new FileAdapter()
);
Run Code Online (Sandbox Code Playgroud)
这有助于我们:
轻松维护代码: 正如您已经看到的,如果类的依赖项之一发生变化,我们不需要修改该类。
使代码更具可测试性:当您运行测试套件时,MySQLAdapter您不想在每个测试中访问数据库,因此 PDO 对象将在测试中被模拟:
// test snippet
$log = new Logger(
new MySQLAdapter(
$this->getMockClass('PDO', [...])
)
);
Run Code Online (Sandbox Code Playgroud)问:怎么Logger知道你给他的课程是它需要的,而不是一些垃圾?
A:这是接口(AdapterInterface)作业,它是与其他类之间的契约Logger。Logger“知道”实现该特定接口的任何类都将包含完成其工作所需的方法。
您可以将此类(即:容器)视为存储运行应用程序所需的所有对象的中心位置。当您需要其中之一时,您可以从容器中请求对象,而不是自己实例化。
你可以把 DiC 看作是一只被训练出来的狗,它可以出去拿报纸,然后把它带回来给你。问题是这只狗只在前门打开的情况下接受过训练。只要狗的依赖性不会改变(即门打开),一切都会好起来的。如果有一天前门关闭,狗将不知道如何拿到报纸。
但是如果狗有一个 IoC 容器,他可以找到一种方法......
正如你之前看到的,“经典”代码的初始化过程是:
IoC 简单地复制了上述过程,但顺序相反:
如果您认为依赖注入是某种 IoC,那么您是对的。当我们谈论依赖注入时,我们有这个例子:
// log to mysql
$log = new Logger(
new MySQLAdapter(
new PDO()
)
);
Run Code Online (Sandbox Code Playgroud)
乍一看,有人可能会说实例化过程是:
问题是代码将从中间向左解释。所以顺序是:
IoC 容器只是使这个过程自动化。当您Logger从容器请求时,它使用PHP 反射和类型提示来分析其依赖项(来自构造函数),实例化所有依赖项,将它们发送到请求的类并返回一个Logger实例。
注意:为了找出一个类有什么依赖关系,一些 IoC 容器使用注解而不是类型提示或两者的组合。
所以要回答你的问题:
如果您的容器可以自行解决依赖项,但由于各种原因您还需要手动添加更多依赖项,则可以在初始化容器后的启动过程中执行此操作。
注意: 在野外也有各类这两个原则之间的混合的,但我试图解释你什么是他们每个人背后的主要思想。您的容器的外观完全取决于您,只要您出于教育目的,就不要害怕重新发明轮子。