我知道,这是一个老问题。但我目前正在寻找一个类似但更普遍的问题的答案:如何在工厂中正确实现 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,您如何看待这个建议的方法?