依赖注入总是正确的答案吗?

Is Dependency Injection always the right answer?

我目前正在考虑很多关于依赖注入的问题,它的优点和缺点。一般来说,在大多数情况下,依赖注入似乎是正确的选择。我看到它在哪里派上用场以及它如何使代码更具可读性。因此,我正在尝试使用尽可能多的依赖注入来创建我的 classes,将关注点分离到多个对象中。在我 70% 的日常工作中,这很好。它有效,我看到了好处。

不过剩下的30%让我很纠结。我对依赖注入本身的概念没有问题,但事实上我认为 PHP 确实有一些 "special properties" 让我怀疑依赖注入是正确的选择。

  1. 使用 DI 而不是服务定位器的主要观点似乎是 "compile-time errors" 而不是 "run-time errors"。我明白了,对于像 Java 或 C 这样的语言,但是在 PHP 中没有 "compile-time error" 这样的东西不会自动导致 "run-time error"。至少我没遇到过。

  2. 在启动一次的程序中,将它们的源代码加载到内存中并在执行期间一直保留在那里我明白您为什么要使用 DI。这是有道理的,您不必(通常)担心应用程序需要多长时间才能达到 运行ning 状态,并且您应该(通常)有足够的内存来将所有代码保存在那里。所以加载所有依赖项并将它们保存在内存中似乎没问题。但是,PHP 脚本在 Apache、NGINX 或其他上下文中使用时,每次用户访问它时都会启动。除此之外,我们希望我们的应用程序 运行 尽可能快,使用尽可能少的资源来充分利用服务器硬件。问题是,如果我每次都加载整个库,即使我只访问一小部分代码,它看起来也很浪费......(我在下面有一个例子)

  3. 如上所述,我尽量使用DI。但剩下的 30% 我仍然使用服务定位器模式处理,因为我 a.) 只需要在特定条件下的依赖或 b.) 我访问或多或少可替代的 service/helper class具有全局功能。 (见下面的例子)

在这方面,我读了很多关于 a.) Helper classes 是邪恶的 b.) 当你在你的 class 中只使用一次依赖时,你应该将它分成一个分开 class (老实说,这是我不太明白的一点,因为当你使用 DI 时,无论如何你都必须创建 class 所以为什么要把它从一般的 class 中分离出来?).

我创建了一些虚拟代码来展示我认为(在撰写本文时)服务定位器(在 PHP)比 DI 更明智的 30%。

class RecordHandler {
    // Use a trait to enable access to the getInstanceOf() method,
    // which is a link to the global instance of the service container singleton
    use ContainerAwareTrait;

    /**
     * @var EnvService
     */
    protected $envService;

    /**
     * RecordHandler constructor.
     *
     * @param EnvService $envService
     */
    public function __construct(EnvService $envService) {
        $this->envService = $envService;
    }

    // ... Other methods...

    public function filterRecords(array $records): array {
        // Using the normal DI object...
        if($this->envService->isStaging()){
            // Do something different ...
            return [];
        }

        foreach($records as $recordId => $record){
            // This can happen in 10% of the times...
            // I don't know here if the database is already connected. 
            // The factory registered in the container will initialize the connection when the object is created...
            if(empty($record)){
                $record = $this->getInstanceOf(DbService::class)->query("SELECT * FROM `stored_record_table WHERE id = ?", [$recordId]);
            }

            // This can happen, but it does not happen in 50% of the cases...
            if(!empty($record["xmlField"])){
                $record["xmlField"] = $this->getInstanceOf(XmlService::class)->handleXml($record["xmlField"]);
            }

            // Creating a class instance based on the record's value...
            if(!empty($record["classField"]) && class_exists($record["classField"]) && 
                in_array(HandlerInterface::class, class_implements($record["classField"]))) {
                $i = $this->getInstanceOf($record["classField"]);
                $i->handle($record);
            }
        }
    }

请注意,EnvService 和 DbService 在使用的​​服务容器中被标记为 "Singleton"。这意味着一旦它们被创建,容器将 return 一遍又一遍地使用相同的实例。

我真的很想知道是否有更好的解决方案,因为我测试了多种解决方案,这样做可以节省大约 30-50% 的执行时间,并根据记录的种类而有所波动已处理。

我也知道隐藏依赖项等缺点。为了防止这个问题,我尝试创建 "context objects",它们基本上是硬编码的 DI 容器,但它们似乎也不是选项。

最后我尝试使用 "lazy loading" 来避免上述问题,但是 PHP-DI 在其文档中明确表示您不应为超过 3-4 classes,这似乎没有多大帮助。

没有规律总是答案;当无法推迟时,做出明智、务实的架构决策非常重要。

您说得对,在 PHP 的情况下,由于必须在每个请求上实例化整个对象图,DI 可能会导致性能下降。

然而,依赖注入显然与性能无关。它是一种实现控制反转并最终实现模块化设计、关注点分离、可重用性和 可测试性 的方法。这些好处大大超过了 "compile-time" 错误捕获能力。

在软件项目的早期阶段,预测它是否足够简单(您的 30%)放弃所有这些好处并采用互连设计而不是模块化是非常困难和冒险的一,运动部件紧密耦合,只是名义上或性能上。