PHP中的控制反转

8
我刚开始使用依赖注入,因为很明显,在没有阅读控制反转(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();
    }
}

在这些内容已经在应用程序结构中的情况下,我会使用依赖注入方式对传入的参数进行类型提示,但如果没有这个,我可以这样做:

DepContainer::register('Bar', function(){
    return new Bar();
});

$f = new Foo();
$f->say();

对我来说,在bootstrap中注册所有依赖项是更加清晰的方式。在运行时,像我展示的那样,我认为这样做和new Foo(new Bar(...)...)一样丑陋。

1
我将在几分钟内完成...向大家道歉。 - LouieV
我并没有真正使用过这个设计模式,但是好问题,赞一个。我认为只要是“我是否以良好的方式注入了类”,而不是“请审核我的代码”,我认为这个问题适合在这个网站上讨论。 - halfer
我真的不喜欢冗长的部分,在一个大型应用程序中,构造函数注入可能会很快变得混乱。此外,我认为使用setter注入的问题在于程序员需要在对类进行操作之前设置依赖项。 - LouieV
1个回答

23
我将尝试总结一些你应该知道的内容,并希望能够解决你的一些困惑。让我们从一个基本的例子开始:
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
// not good
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 to mysql
$log = new Logger(
    new MySQLAdapter(
        new PDO()
    )
);

正如您所看到的,我们在构造函数中没有实例化任何东西,而是将已实例化的类传递给构造函数。这使我们能够替换任何依赖项而不修改该类:
// log to file
$log = new Logger(
    new FileAdapter()
);

这有助于我们:

  1. To easily maintain the code: As you already saw, we don't need to modify the class if one of its dependencies changed.

  2. 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容器,它就能找到方法...

控制反转

到目前为止,"经典"代码的初始化过程如下:

  • 我们创建Logger
    • Logger创建MySQLAdapter
      • MySQLAdapter创建PDO

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

  • 创建PDO
    • 创建MySQLAdapter并将其提供给PDO
      • 创建Logger并将其提供给MySQLAdapter

如果您认为依赖注入是某种IoC,那么您是正确的。当我们谈论依赖注入时,我们有以下示例:

// log to mysql
$log = new Logger(
    new MySQLAdapter(
        new PDO()
    )
);

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

  • 创建Logger
  • 创建MySQLAdapter
  • 创建PDO

事实上,代码将从中间向左解释。因此顺序将是:

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

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

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

因此,回答您的问题:

  • 如果容器能够自行解析依赖项,则只需要在应用程序的引导过程中实例化容器即可。(请参见控制反转容器
  • 如果容器无法自行解析依赖项,则需要手动提供运行应用程序所需的对象。这种提供通常发生在引导过程中。(请参见依赖注入容器)

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

注意:在实际应用中这两个原则之间会有各种混合,但我试图向您解释它们背后的主要思想。 您的容器将如何呈现仅取决于您,不要害怕重新发明轮子,只要为教育目的而这样做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接