【问题标题】:Dependency injection in factories工厂中的依赖注入
【发布时间】:2014-08-22 12:48:56
【问题描述】:

我对 DI 很陌生,但我真的很想尝试使用它。

有些东西我不明白。下面是一个简单的工厂伪代码,我用的比较多。

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 容器。

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

【问题讨论】:

  • 如果您需要在运行时创建这些页面,听起来就像您所做的那样,那么您提出的解决方案是一种流行的解决方案。我真的不认为你在这里有问题。你做的事情是正确的。
  • 谢谢,但是那些页面有很多依赖,这使得工厂的依赖也很重。所以我想也许 DI 容器可能对我有很大帮助。我是不是期待太多了?
  • 我想我不完全确定你在问什么。您的真正问题是构造函数依赖项过多吗?如果是这样,Mark Seemann 在他关于重构聚合服务的博客中解决了这个问题:blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices
  • 我只是想使用 DI 容器而不是自己创建实例,并且仍然具有上述相同的行为:根据简单的运行时条件获取特定实例。
  • DI 容器对象/应用程序的重点是“配置”吗?还是出于可测试性的原因,所以您不必对依赖项进行硬编码并能够在运行时更改它们?

标签: php oop dependency-injection factory


【解决方案1】:

注意:依赖注入是一种设计模式,而 DI 容器是库,它们通过依赖注入来生成实例......或者它们是糟糕的服务定位器,有人在 出售 作为最新的热门话题

正确实施的 DI 容器基本上是一个“智能工厂”。但是实施一个可能会超出您当前的能力。这有点复杂,因为一个好的 DI 容器构建整个依赖树。

例如:

假设您有一个类Foo,它需要在构造中传递AlphaBeta 的实例。但是有一个问题。 Beta 的实例在构造函数中还需要一个 PDO DSN 和一个 Cache 的实例。

一个制作精良的 DI 容器将能够一次构建整个依赖树。

您应该使用现有的 DI 容器,而不是自己制作。

我的建议是Auryn

【讨论】:

  • 我不打算自己写一个。对不起,如果它令人困惑。我想使用 PHP-DI,但对我来说,其中许多看起来几乎相同。 Auryn 也有一个字符串用法,但它是 PHP,我也没有看到更好的解决方案,这就是为什么我想尽可能少地使用它们。
  • @SinistraD 我不完全确定您所说的“严格使用”是什么意思。特别是因为 php 的new 运算符也可以描述为"somewhat stringy"。而且没有干净的方法可以覆盖它。
【解决方案2】:

我知道,这是一个老问题。但我目前正在寻找一个类似但更普遍的问题的答案:如何在工厂中正确实现 DI 模式,以决定它们在运行时直接创建什么以及如何创建?也许我的回答可以帮助某人,他们会在搜索引擎中找到这个问题(就像我一样)?而且,也许,它对你也有用?或者,您将分享您在过去近两年中获得的一些经验……我希望,您已经不像您在 SO 上提出这个问题时那样“对 DI 感到陌生” :) (我目前是 DI 新手)

我发现这是一个常见的问题,但没有一个常见的答案,尤其是在 PHP 中。例如,在 Guice(Google 流行的 Java 框架,支持 DI)中应该如何解决这个问题:

有人建议直接在这样的工厂中“新建”对象(使用“new”运算符创建),他们说这没问题。例如。 Miško Hevery 是 AngularJS(来自 Google 的流行 JS 框架)的两位原始开发者之一,在his article «To “new” or not to “new”…» 中介绍了他自己的分离原则:他说,可以随时随地创建“价值对象”,而“服务对象” ” 只能通过DIC注入,不能直接创建。

但我个人不同意它们,因为这样的工厂可能有一些业务逻辑,这使得不可能将它们视为应用程序组合根的一部分(仅允许更新)。

解决方案:将琐碎的工厂注入工厂

IMO,遵循 DI 模式的唯一解决方案是创建特殊的普通“注入友好”工厂,这些工厂依赖于注入器并返回它们直接从调用注入器的方法获得的对象。对注入器的直接访问,就像直接更新自己的依赖项一样,只允许在组合根中进行,因此,所有这些提供者的声明都应该在组合根中完成。我将通过以下示例演示我的建议。

您写道您将使用 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(或其他页面)的所有依赖项,如果没有此类依赖项,这可以,但如果它们很多,则不可取。 IMO 最好注入注入器并使用它创建对象:这允许手动指定我们的琐碎工厂——而不是其他依赖项。

那么,@SinistraD,您如何看待这个建议的方法?

【讨论】:

    【解决方案3】:

    已编辑

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

    DI 作为路由器输出处理程序

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

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

    所以它看起来像这样:

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

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

    ::类常量

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

    【讨论】:

    • 你解决了一个错误的问题。所以如果你有 20 页(如果你想在以后添加更多),你可以写 20 cases?您应该了解的内容称为Front Controller (it utilizes Dispatching, Routing and Class autoloading)
    • 感谢您的提示。我曾经这样做过,在我的前端控制器中进行简单的路由,并且我的页面会自动实例化,但它是不可测试的。我不得不让构造函数为空,并使用全局的单例服务定位器来获取资源。我无法隔离这些页面,也无法测试它们。而且这些工厂还不错,他们有一些案例,但我可以有很多这样的,在模块中分组页面等,很难维护这些的依赖列表。但是,如果您知道通过路由和可测试页面解决此问题的方法,我将非常感谢您提供详细的答案。
    • 所以你可以说这是我的前端控制器的路由。但是,是的,我觉得我正在解决错误的问题。这就是我问的原因。
    猜你喜欢
    • 2012-08-09
    • 1970-01-01
    • 1970-01-01
    • 2023-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-01
    • 1970-01-01
    相关资源
    最近更新 更多