PHP控制反转

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。

这个过程是这样工作的:

  • 我们创建了记录器
    • 记录器创建 MySQLAdapter
      • 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)

这有助于我们:

  1. 轻松维护代码: 正如您已经看到的,如果类的依赖项之一发生变化,我们不需要修改该类。

  2. 使代码更具可测试性:当您运行测试套件时,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 容器,他可以找到一种方法......

控制反转

正如你之前看到的,“经典”代码的初始化过程是:

  • 我们创建了记录器
    • 记录器创建 MySQLAdapter
      • MySQLAdapter 创建 PDO

IoC 简单地复制了上述过程,但顺序相反:

  • 创建 PDO
    • 创建 MySQLAdapter 并给他 PDO
      • 创建 Logger 并给他 MySQLAdapter

如果您认为依赖注入是某种 IoC,那么您是对的。当我们谈论依赖注入时,我们有这个例子:

// log to mysql
$log = new Logger(
    new MySQLAdapter(
        new PDO()
    )
);
Run Code Online (Sandbox Code Playgroud)

乍一看,有人可能会说实例化过程是:

  • 创建记录器
  • 创建 MySQLAdapter
  • 创建 PDO`

问题是代码将从中间向左解释。所以顺序是:

  • 创建 PDO
    • 创建 MySQLAdapted 并给他 PDO
      • 创建 Logger 并给他 MySQLAdapter

IoC 容器只是使这个过程自动化。当您Logger从容器请求时,它使用PHP 反射类型提示来分析其依赖项(来自构造函数),实例化所有依赖项,将它们发送到请求的类并返回一个Logger实例。

注意:为了找出一个类有什么依赖关系,一些 IoC 容器使用注解而不是类型提示或两者的组合。

所以要回答你的问题:

  • 如果容器可以自行解决依赖关系,则您只需在应用程序启动过程中实例化容器。(参见控制反转容器
  • 如果容器无法自行解决依赖项,您将需要使用运行应用程序所需的对象手动配置容器。这种配置通常发生在引导过程中。(参见依赖注入容器)

如果您的容器可以自行解决依赖项,但由于各种原因您还需要手动添加更多依赖项,则可以在初始化容器后的启动过程中执行此操作。

注意: 在野外也有各类这两个原则之间的混合的,但我试图解释你什么是他们每个人背后的主要思想。您的容器的外观完全取决于您,只要您出于教育目的,就不要害怕重新发明轮子