访问 DI 容器

10

我正在启动一个新项目并设置基础工作。有几个问题出现了,我可能会在这里问很多问题,希望我能找到一些答案。

第一步是处理对象的依赖关系。我决定使用依赖注入设计模式来处理应用程序中的所有内容,但对此我还有些不太熟悉。

在实际编码时,我遇到了一个问题。如果一个类有多个依赖项,并且你想通过构造函数传递多个依赖项(以便在实例化对象后不能更改它们),那么该怎么办呢?

如何在不使用传递依赖项数组、call_user_func_array()、eval()或Reflection的情况下完成呢?这就是我要寻找的答案:

<?php

class DI
{
    public function getClass($classname)
    {
        if(!$this->pool[$classname]) {
            # Load dependencies
            $deps = $this->loadDependencies($classname);

            # Here is where the magic should happen
            $instance = new $classname($dep1, $dep2, $dep3);

            # Add to pool
            $this->pool[$classname] = $instance;

            return $instance;
        } else {
                return $this->pool[$classname];
        }
    }
}

我希望避免调用类的最昂贵方法,还有其他建议吗?

此外,在需要访问不同模型的控制器等类中如何访问DI类? 我应该静态调用它还是将其传递给每个需要它的类? 我认为最后一个想法是不可行的。

谢谢大家。

3个回答

23

[在我开始之前,让我说一下,我主要是一个Java程序员 - 只有一点PHP知识。但我会尝试在没有语言具体性的情况下传达最重要的概念。]

依赖注入基于代码的两个部分:

  1. 构建
  2. 执行

在其最极端的形式中,执行部分中找不到任何new运算符。所有这些都被移动到构建部分。(实际上,这将被减弱)

所有的构建操作都发生在构建部分。它从下至上创建了执行所需的对象图。那么假设,它应该构建A:

  • A依赖于B, 和
  • B依赖于C。

然后

  • 首先构造C。
  • 然后用C作为参数构造B。
  • 最后用B作为参数构造A。

因此,C不必作为构造函数参数传递给A。这个小例子并没有充分说明它可以将需要传递的对象数量大大减少。

依赖注入器本身不应该传递到执行部分。这是每个人(包括我自己)在第一次接触到DI时都会犯的基本错误之一。问题在于,这将完全模糊构建和执行之间的界线。另一种说法是,它将违反Demeter法则。或者按照模式的说法:最终会“降级”依赖注入模式为服务定位器模式。这是否真的是降级还有争议,但无论如何,通常不应该滥用依赖注入器作为服务定位器

每当您需要在执行过程中使构造对象具有产生其他对象的能力时,您只需传递简单的提供者(Java DI框架Guice使用的术语),而不是传递依赖注入器。这些是相当简单的类,只能创建某种类型的对象。它们与工厂有相似之处。

首先尝试直接将所需的依赖项传递给构造函数。

因此,总结一下:

  • 自下而上构建对象。
  • 仅传递创建对象所需的尽可能少的依赖项。
  • 完成后开始执行。
  • 在执行期间,仍可以通过使用提供程序来获取新创建的对象。

但不要走得太远:简单的对象仍然可以在没有提供者的情况下创建:-)

现在,您唯一需要做的就是将这些东西翻译成优质代码。也许其他人可以帮助您提供一些 PHP 示例。

补充说明:更多关于提供程序的信息

如上所述,“Provider”(专用工厂)的概念是Java DI框架Guice的一个特定部分。该框架可以自动为任何类型的对象创建提供程序。但是,该概念通常对DI有用。唯一的区别是,如果没有Guice或类似框架的帮助,则必须自己编写提供程序-但这很容易:

假设B依赖于C。

  • 如果B只需要一个固定的C实例,则不需要提供程序-您可以简单地使用构造函数参数C构建B。
  • 如果B在执行过程中需要创建更多的C实例,则只需编写一个名为CProvider的类,其中包含一个可以创建新实例的get()方法。然后将CProvider实例传递到B的构造函数中,并将其存储在B的实例字段中。现在,当B需要新的C实例时,可以调用cProvider.get()

提供者是构建代码的一部分,所以你可以使用new C(...)!另一方面,它们不属于执行代码的一部分,因此不应该在那里有任何执行逻辑。

CProvider 可以传递到多个构造函数中。你也可以编写多个版本的 CProvider1CProvider2 等,每个版本都可以使用不同的属性构造 C 对象的不同版本。或者你可以简单地使用不同的参数实例化 CProvider 多次。


感谢你的解释,非常有见地。我了解服务定位器模式,但这不是我想要实现的。你能否详细解释一下提供程序(Providers)的使用方法? - Andre
1
我认为,最初的努力是值得的 - 因为当使用 DI(无论是否使用框架)时,您最终将拥有组件,甚至可以在下一个应用程序中重复使用!从长远来看,这是 DI 最大的好处,甚至比代码可测试性的提高更重要 :-) - Chris Lercher
1
太棒了,对于C#,我喜欢只使用Func<T>作为我的提供程序。这非常简单。 - Mark Lindell
关于提供者,我们通常在依赖项直到运行时(例如来自用户输入)才能确定,即使只有一个依赖项时,也会使用它们。我想知道除了您上面所写的内容外,您是否也这样做? - WW.
@WW:是的,如果依赖项的范围比您注入它的对象更窄,则这是必要的(这通常发生在与旧框架集成时)。如果您可以将它们保持在相同的范围内,则在大多数情况下,范围应该足以管理此操作,而无需提供程序。(再次强调,我有点考虑Guice,因此这里是相应的文档链接:http://code.google.com/p/google-guice/wiki/Scopes) - Chris Lercher
显示剩余2条评论

2
你应该考虑使用一个IOC容器来管理你的依赖关系。一个好的IOC容器应该能够自动传递依赖项给相关的构造函数。
已经有一个问题询问PHP的IOC容器选项。

2

看起来你正在尝试自己编写依赖注入容器。为什么不使用已经存在的容器,比如SymfonyCrafty或者Sphicy呢?


目标是尽可能避免使用第三方库,除非绝对必要(主要是由于许可问题)。 - Andre
@andre:Symfony是根据MIT许可证授权的。阅读它,基本上它声明“你想干什么就干什么”:http://www.opensource.org/licenses/mit-license.php - Wim Coenen
3
我正在创建自己的许可证,并且不想向第三方库或软件归属原作者(这是项目要求的一部分),因此我需要创建自己的容器。 - Andre

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