工厂中的依赖注入

6

我很新于依赖注入,但我真的想尝试使用它。

有一些我不理解的地方。以下是我经常使用的工厂的简单伪代码。

class PageFactory {
   public function __construct(/* dependency list */) {
      ... //save reference to the dependencies
   }

   public function createPage($pagename) {
       switch ($pagename) {
           case HomePage::name:
               return new HomePage(/* dependency list */);
           case ContactPage::name:
               return new ContactPage(/* dependency list */);
           ...
           default:
               return null;
       }
   }
}

这个逻辑非常简单,它根据一个字符串来选择实现实例。这非常有用,因为我可以在稍后的时间内选择我需要的页面,并且只会创建那个页面。

我该如何重写这段代码,以便我的页面实例将通过依赖容器创建,因此我就不需要处理工厂和其创建的页面的依赖关系了?

唯一的解决方案是使我想要使用的容器成为工厂的依赖项,并从工厂内部调用它。但我有很多问题。

首先,我不想将容器耦合到我的应用程序中,也不想将容器耦合到每个工厂中。

其次,而我最大的问题是,对于容器的调用非常混乱,它是以字符串形式(即$container->get('Foo');)存在的。我希望尽可能少地使用它。如果可能,只使用一次。

编辑:

我不想编写DI容器,我想使用现有的容器。我的问题是关于用法的。在上述工厂中,如何使用DI容器或将其嵌入其中,同时保持实例选择的逻辑。

编辑2:

我开始使用Dice作为DI容器,因为它很轻量级并且知道我需要的一切。我希望能在一个地方使用它并构建整个应用程序。为此,我需要一种方法来消除这些工厂,或以某种方式修改它们,使这些页面像依赖项一样运行,以便DI容器可以为它们提供实例。

编辑3:

是的,我需要这个用于测试目的。我还不熟悉测试,但到目前为止,它非常棒,我真的很喜欢。

这些页面是MVC框架所称的控制器。但我查看的所有MVC框架都没有使其控制器可测试性,因为它们自动创建它们的实例。由于它们是由系统创建的,用户无法定制它们的构造函数参数。

有一种简单的方法可以检查任何框架的情况。我只需查找在特定框架中应该如何在控制器中使用数据库的方式。大多数框架要么是过程化的,要么使用一些服务定位器,无论哪种方式,它们都从公共范围获取其依赖项,这是我不想做的。这就是为什么我没有自动化控制器实例化的原因。缺点是现在我有了这些奇怪的工厂,它们携带很多依赖项。我想将此任务代替DI容器。

大多数框架都实现了自己的测试机制,这更像是功能测试,而不是单元测试,但我也不想这样做。


如果您需要在运行时创建这些页面,就像您所说的那样,那么您想出的解决方案是一个流行的解决方案。我真的不认为您在这里有问题。您正在正确地处理事情。 - jmrah
谢谢,但是那些页面有很多依赖项,这也使得工厂对依赖项的依赖非常重。所以我想也许一个 DI 容器可以极大地帮助我。我期望太多了吗? - Máthé Endre-Botond
我想我不是完全确定你在问什么。你真正的问题是构造函数依赖太多吗?如果是这样,Mark Seemann在他关于重构聚合服务的博客中有所涉及:http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/。 - jmrah
我只是想使用一个依赖注入容器来代替手动实例化对象,同时仍然可以实现上述相同的行为:根据简单的运行时条件获取特定的实例。 - Máthé Endre-Botond
依赖注入容器对象/应用程序的目的是“配置”吗?还是出于可测试性的原因,以便您不会硬编码依赖项并能够在运行时更改它们? - Weltschmerz
这是为了可测试性。我不太理解第一个原因。 - Máthé Endre-Botond
3个回答

3
请注意:依赖注入是一个设计模式,而DI容器则是库,通过利用依赖注入来生成实例…或者他们就是那些被某些人“卖”成最新潮流的垃圾服务定位器。
一个正确实现的DI容器基本上就是一个“智能工厂”。但实现一个可能远远超出了你目前的能力范围。这种东西真的很复杂,因为一个好的DI容器必须构建整个依赖关系树。
例如:
假设您有一个类Foo,它需要在构造函数中传递AlphaBeta的实例。但存在一个问题,Beta的实例也需要在构造函数中传递PDO DSN和Cache的实例。
一个制作精良的DI容器将能够一次性构建整个依赖关系树。
所以,你不应该自己去做一个,而应该选择已经存在的DI容器。
我的建议是Auryn

我本来没有计划自己写一个。如果让你感到困惑,我很抱歉。我想使用PHP-DI,但对我来说,它们中的许多看起来几乎相同。Auryn的使用也有点复杂,但它是PHP,我也没有看到更好的解决方案,这就是为什么我希望尽可能少地使用它们的原因。 - Máthé Endre-Botond
@SinistraD 我并不完全确定你所称的 "stringy usage" 是什么意思。特别是因为 PHP 的 new 运算符也可以被描述为 "有点像字符串"。而且并没有干净的方法来覆盖它。 - tereško

1
我知道,这是一个老问题。但我目前正在寻找一个类似的答案,更加普遍的问题:如何在运行时正确地实现决定其创建什么和如何创建的工厂中的DI模式?也许我的答案可以帮助那些在搜索引擎中找到这个问题的人(就像我一样)?也许,对你也有用?或者,你会分享一些你在这近两年中获得的经验……我希望你现在不再像当初在SO上问这个问题时那么“新手DI”了 :)(我目前对DI还很陌生)
我发现这是一个常见的问题,但在PHP中没有一个通用的答案。例如,这个问题在Guice(Google流行的支持DI的Java框架)中应该如何解决: 有人建议在工厂中直接使用“new”运算符创建对象,他们认为这是可以的。例如,AngularJS的两位原始开发人员之一——来自谷歌的流行JS框架Miško Hevery,在他的文章“新还是不新…”中介绍了自己的分离原则:他说,无论何时何地需要创建“值对象”,都可以创建,而“服务对象”只能通过DIC注入,不允许直接创建。
但我个人不同意他们的观点,因为这样的工厂可能具有一些业务逻辑,使其不可能被视为应用程序组合根的一部分(其中仅允许使用newing-up)。
解决方案:将琐碎的工厂注入到工厂中。

在我看来,遵循DI模式的唯一解决方案是创建特殊的“注入友好型”工厂,它们依赖于注入器并返回直接从调用注入器方法获取的对象。由于直接访问注入器(如自己依赖关系的直接new-up)只允许在组合根中进行,因此所有这些提供程序的声明都应该在组合根中完成。我将通过以下示例演示我的建议。

你写道你将使用PHP-DI作为DIC。我也是,我决定在我的项目中使用它,因此下面的示例也将使用它。

// 1. First, define interfaces of trivial factories that'll be used to
// create new objects using injector.
interface HomePageTrivialFactoryInterface {
    public function __construct(
        DI\Container $container
        // Injector is needed to fetch instance directly from it.
        // List of other dependencies that are already known at design
        // time also goes here.
    );
    public function __invoke(
        // List of dependencies that are computed only in runtime goes here
        // You may name this method something else, “create” for example,
        // but then you'll also have to specify this method's name when
        // you'll wire things together in container definitions on step #3.
    ): HomePage;
}
// ContactPageTrivialFactoryInterface is defined similarly

// 2. Now in PageFactory::createPage we'll use the injected trivial
// factories to create page objects.
class PageFactory {
    private $homePageTrivialFactory;
    private $contactPageTrivialFactory;

    public function __construct(
        HomePageTrivialFactoryInterface $homePageTrivialFactory,
        ContactPageTrivialFactoryInterface $contactPageTrivialFactory
        // list of other dependencies that are already known at design time
        // also goes here
    ) {
        // save reference to the dependencies
    }

    public function createPage(
        $pagename
        // list of other dependencies that are computed only at runtime goes
        // here
    ) {
        switch ($pagename) {
            case HomePage::name:
                return ($this->homePageTrivialFactory)(
                    // Write here all the dependencies needed to create new
                    // HomePage (they're listed in
                    // HomePageTrivialFactoryInterface::get's definition).
                    // Here you may use both the dependencies obtained from
                    // PageFactory::__construct (known at design time) and
                    // from PageFactory::createPage methods (obtained at
                    // runtime).
                );
            case ContactPage::name:
                return ($this->contactPageTrivialFactory)(
                    /* dependency list, similarly to HomePage */
                );
            // ...
            default:
                return null;
        }
    }
}

// 3. Now, let's set up the injection definitions in the composition root.
// Here we'll also implement our TrivialFactoryInterface-s.
$containerDefinitions = [
    HomePageTrivialFactoryInterface::class => DI\factory(
        function (DI\Container $container): HomePageTrivialFactoryInterface
        {
            return new class($container)
                implements HomePageTrivialFactoryInterface
            {
                private $container;

                public function __construct(
                    DI\Container $container
                    // list of other design time dependencies
                ) {
                    // save reference to the dependencies
                }

                public function __invoke(
                    // list of run time dependencies
                ): HomePage
                {
                    return $this->container->make(HomePage::class, [
                        // list of all dependencies needed to create
                        // HomePage goes here in the following form.
                        // You may omit any dependency and injector will
                        // inject it automatically (if it can).
                        // 'constructor parameter name of dependency' =>
                        //     $constuctor_parameter_value_of_dependency,
                        // etc - list here all needed dependencies
                    ]);
                }
            };
        }
    ),
    // ContactPageTrivialFactoryInterface is defined similarly
];

// 4. Finally, let's create injector, PageFactory instance and a page using
// PageFactory::createPage method.
$container = (new DI\ContainerBuilder)
    ->addDefinitions($containerDefinitions)
    ->build();
$pageFactory = $container->get(PageFactory::class);
$pageFactory->createPage($pageName);

在上面的示例中,当我将简单工厂连接到DI容器时,我声明了这些工厂的接口,并使用内联匿名类实例对其进行了实现(此功能是在PHP 7中引入的)。如果您不想麻烦自己编写这些接口,可以跳过这一步,直接编写这些工厂,而无需接口。简化的示例如下所示。请注意,在示例中我省略了步骤1、2和4:步骤#1被删除,因为我们不再需要定义那些琐碎的接口,步骤2和4保持不变,只是从PageFactory构造函数中删除了类型提示,引用已不存在的接口。唯一改变的步骤是第3步,如下所示:
// 3. Now, let's set up the injection definitions in the composition root.
// Here we'll also implement our TrivialFactory-s and wire them to
// PageFactory constuctor parameters.
$containerDefinitions = [
    PageFactory::class => DI\object()
        ->constructorParameter('homePageTrivialFactory', DI\factory(
            function (
                DI\Container $container
                // list of other dependencies that are already known at
                // design time also goes here
            ) {
                function (
                    // list of run time dependencies
                ) use($container): HomePage
                {
                    return $container->make(HomePage::class, [
                        // list of all dependencies needed to create
                        // HomePage goes here in the following form:
                        // 'constructor parameter name of dependency' =>
                        //     $_constuctor_parameter_value_of_dependency,
                        // etc - list here all needed dependencies
                    ]);
                }
            }
        ))
        // ContactPageTrivialFactory is wired and defined similarly
    ,
];

最后,如果您认为在应用程序的组合根中新建对象是可以接受的(这可能确实可以接受),那么您也可以在这些微不足道的工厂中执行此操作,而不是注入注射器并使用注射器创建实例。但在这种情况下,您还必须手动实例化HomePage(或其他页面)的所有依赖项,如果没有这样的依赖项,则可以,但如果有很多依赖项,则不可取。我认为最好注入注射器并使用它来创建对象:这允许我们仅手动指定我们微不足道的工厂,而不是其他依赖项。@SinistraD,您对这种建议的方法有何看法?

0

编辑过的内容

使用 DI 容器几天后,我意识到解决方案实际上是多么简单,现在我真的感到很尴尬。bad_boy 还推荐了路由。

DI 作为路由输出处理程序

我可以使用 DI 容器来处理简单路由的输出。路由的问题在于它们向框架返回一个类名,因此由框架来实例化它们。这是个问题,因为构造函数将被预定义(或者只是空的),依赖项只能来自公共范围或服务定位器。

但在 DI 容器的情况下,页面已经由框架而不是用户创建。因此,解决方案就是允许这样的路由存在,但让 DI 框架处理输出。

所以它看起来会像这样:

$router = $di->create(Router::class);
$pageClassName = $router->getRequestedPageClassName();
$page = $di->create($pageClassName);
echo $page->render();

我在应用程序的根目录中使用DI,这样我就可以拥有许多包含任何逻辑和依赖项的路由器,以及任意数量的具有任何依赖项的页面。

::class常量

我也遇到了这些问题。主要是它们只适用于PHP 5.5。我通过编写一个小型的PHP预处理器来解决这个问题,该预处理器接受一个PHP文件,将每个ClassName::class更改为"ClassName",将其保存到我的IDE不可见的特殊位置,并设置我的自动加载程序仅加载已处理的PHP文件。现在,我可以在我的PHP 5.3设置中使用::class常量,只需在.php之前向PHP文件添加一个特殊扩展名即可。


1
你正在解决一个错误的问题。所以,如果你有20页(并且如果你将来想要添加更多),你会写20个case吗?你应该学习的是Front Controller(它利用Dispatching、Routing和Class autoloading) - Yang
谢谢你的建议。我曾经在我的前端控制器中实现了简单路由,我的页面自动实例化了,但是它无法进行测试。我不得不使构造函数为空,并使用全局、单例服务定位器来获取资源。我没有办法隔离这些页面,也无法对它们进行测试。而这些工厂并不是那么糟糕,它们有一些情况,但我可以有很多这样的工厂,将页面分组到模块中等等,只是很难维护这些依赖列表。但如果你知道一种解决这个问题的方法,可以通过路由和可测试的页面,我会非常感激详细的答案。 - Máthé Endre-Botond
所以你可以说这是我的前端控制器的路由。但是,我感觉我正在解决错误的问题。这就是为什么我问的原因。 - Máthé Endre-Botond

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