理解IoC容器和依赖注入

64

我的理解:

  • 依赖是指一个 ClassA 实例化新的实例时需要一个 ClassB 实例。
  • 依赖注入是指将 ClassB 的实例传递给 ClassA,可以通过 ClassA 构造函数中的参数或通过 set~DependencyNameHere~(~DependencyNameHere~ $param) 函数来实现。 (这是我不完全确定的领域之一)
  • IoC 容器是一个单例类(在任何给定时间只能实例化1个实例),可以在其中注册创建这些类对象的特定方式。 这里有一个示例链接,其中包括我一直在使用的 IoC 容器的类定义和描述

到目前为止,我开始尝试在更复杂的情况下使用 IoC 容器。目前似乎要使用 IoC 容器,我对于几乎想要创建的任何类都限于具有依赖项,在 IoC 容器中定义该依赖项。如果我想创建继承类,但只有在父类以特定方式被注册在 IoC 容器中创建时,才能创建它怎么办。

例如:我想创建 mysqli 的子类,但我想在 IoC 容器中注册此类,只有在以前在 IoC 容器中注册为特定方式构造的父类创建时才能实例化。我无法想到一个方法来做到这一点而不重复代码(因为这是一个学习项目,我尽可能地保持“纯洁”)。 这里有更多的示例。

因此,这里有一些我的问题:

  • 在不违反OOP原则的情况下,我上面尝试的操作是否可行?我知道在C++中,我可以使用动态内存和复制构造函数来完成它,但我没有找到在PHP中实现这种功能的方法。(我承认除了__construct之外,我很少使用任何其他的魔术方法,但从阅读和__clone中,如果我理解正确,我不能在构造函数中使用它将被实例化的子类作为父类实例的克隆)。
  • 与IoC相关的所有依赖类定义应该放在哪里?(IoC.php文件只需要在顶部添加一堆require_once(“dependencyClassDefinition.php”)吗?我的直觉告诉我有更好的方法,但我还没有想到一个)
  • 我应该在哪个文件中注册我的对象?目前在类定义后的IoC.php文件中执行所有对IoC::register()的调用。
  • 在注册需要依赖项的类之前,我需要在IoC中注册该依赖项吗?因为我直到实例化在IoC中注册的对象时才调用匿名函数,所以我猜想不需要,但这仍然是一个问题。
  • 还有我忽视的需要做或使用的其他事情吗?我正在逐步进行,但我也希望我的代码可重用,并且最重要的是,任何对我的项目一无所知的人都可以阅读和理解它。
1个回答

140

简单来说(因为这不仅是OOP领域的问题),依赖关系是指组件A需要(依赖于)组件B才能完成其预定任务的情况。在这种情况下,这个词也用于描述被依赖的组件。以OOP/PHP术语来说,考虑以下具有义务的汽车类比的例子:

class Car {

    public function start() {
        $engine = new Engine();
        $engine->vroom();
    }

}

Car 依赖于 EngineEngineCar依赖项。但是这段代码有一些问题:

  • 依赖关系是隐式的;直到检查 Car 的代码时才会知道它存在。
  • 类之间耦合度太高;无法用 MockEngine 进行测试,也不能使用扩展原来的 EngineTurboEngine 而不修改 Car
  • 汽车自己能够建造引擎听起来有点傻,是吧?

依赖注入 是通过显式地提供 Car 所需的 Engine 来解决所有这些问题的一种方式:

class Car {

    protected $engine;

    public function __construct(Engine $engine) {
        $this->engine = $engine;
    }

    public function start() {
        $this->engine->vroom();
    }

}

$engine = new SuperDuperTurboEnginePlus(); // a subclass of Engine
$car = new Car($engine);
上面是构造函数注入的例子,其中依赖项(被依赖的对象)通过类构造函数提供给依赖者(消费者)。另一种方法是在 `Car` 类中公开一个 `setEngine` 方法并使用它来注入 `Engine` 的实例。这被称为“setter注入”,通常用于需要在运行时交换的依赖项。 任何非平凡项目都由一堆相互依赖的组件组成,很容易很快地失去对注入哪里的跟踪。依赖注入容器是一个知道如何实例化和配置其他对象、了解它们与项目中其他对象的关系并为您执行依赖注入的对象。这使您可以集中管理所有项目的(互)依赖关系,更重要的是,使您能够在不必编辑代码的大量位置的情况下更改/模拟其中一个或多个依赖项。 让我们放弃汽车比喻,看看OP试图实现的内容作为示例。假设我们有一个依赖于mysqli对象的数据库对象。假设我们想使用一个非常原始的依赖性注入容器类DIC,它公开两种方法:register($name, $callback)以注册在给定名称下创建对象的方式,并且resolve($name) 以从该名称获取对象。我们的容器设置将如下所示:
$dic = new DIC();
$dic->register('mysqli', function() {
    return new mysqli('somehost','username','password');
});
$dic->register('database', function() use($dic) {
    return new Database($dic->resolve('mysqli'));
});

注意我们正在告诉容器从自身获取mysqli的实例来组装Database的实例。然后要获取具有其依赖项自动注入的Database实例,我们只需要执行以下操作:

$database = $dic->resolve('database');

以上是要点。一个更为复杂但仍相对简单易懂的 PHP DI/IoC 容器是 Pimple,建议查看其文档以获得更多示例。


关于 OP 的代码和问题:

  • 不要使用静态类或单例模式来创建容器(或其他任何东西);它们都是邪恶的。可以尝试使用 Pimple 替代。
  • 决定你想让 mysqliWrapper 类继承 mysql 还是依赖它。
  • 通过在 mysqliWrapper 中调用 IoC,你正在将一个依赖关系换成另一个。你的对象不应该知道或使用容器;否则它就不是 DIC 了而是 服务定位器(反)模式。
  • 你不需要在容器中注册一个类之前先 require 它的文件,因为你不知道是否会使用该类的一个对象。所有容器设置都应该集中在一个地方完成。如果你不使用自动加载程序,可以在注册到容器中的匿名函数内部进行 require

其他资源:


4
根据这个例子,你所创建的似乎是工厂,它们被美化成了关联数组(即 $dic 变量)。Pimple 就是一个美化的关联数组。调用 $dic->resolve() 是一种服务定位器,并让您的对象 API 失去了可靠性。尽管您读过 Fowler 和编写清晰代码的文章,但提供 Pimple 和反模式作为解决方案。我认为你需要重新考虑 $database = 的那部分……不过很高兴看到有人表示不要使用静态方法 :-) - Jimbo
@Jimbo 如果您将其用作服务定位器,即将其传递给您的对象,则它只是一个服务定位器,这是我在答案中明确警告的事情。但是,在应用程序入口附近的某个地方使用类似简单容器来构建对象图形并不会出现任何“反模式”问题。顺便说一下,在具有关联数组/哈希映射作为非标量核心类型之一的语言中,有很多东西可以成长为“光荣的关联数组”;-) - lafor
1
哈哈,说得好。但是$database = $dic->resolve('database');和服务定位器的警告似乎没有关联。我并没有意识到使用它会有警告。如果你对这种东西感兴趣,让我来震撼一下你的思维:链接 - Jimbo
1
好的答案,感谢您花时间解释 - 真的帮助我理解了一些应用程序设计的大局等!(不相关的是,您的 $engine->vroom(); 拉出一个很旧的记忆,让我想起了 Atari ST 上的 Vroom 游戏,上帝,我喜欢那个游戏,声音在当时是现象级的,无论如何,回到工作中...) - James
2
超级加速引擎Plus,我在哭 :D 好的例子 :) - Brudka

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