【问题标题】:How to write factories which follow the Dependency Injection pattern?如何编写遵循依赖注入模式的工厂?
【发布时间】:2016-04-07 21:51:07
【问题描述】:

我是 DI 模式的新手,因此,这是我的问题。

4 月 11 日编辑。

如果我有某个工厂实现了一些业务逻辑,并且在运行时创建了它的主要对象和它的依赖项,这取决于给定的输入参数,我应该如何实现它以便它不会违反 DI 原则? (应该避免直接调用 new 操作符来实例化它的主要对象(由这个工厂产生)和它的依赖关系,不是吗?)

(4 月 11 日编辑结束)

考虑以下 CarFactory 示例:

interface IEngine {}

class RegularEngine implements IEngine {}

class AdvancedEngine implements IEngine {}

class Car
{
    private $engine;

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

class CarFactory
{
    public function __invoke(bool $isForVipCustomer = false): Car
    {
        // @todo Here, CarFactory creates different Enginges and a Car,
        // but they should be injected instead?
        $engine = $isForVipCustomer ? new AdvancedEngine : new RegularEngine;
        return new Car($engine);
    }
}

// And here is my composition root:
$carFactory = new CarFactory;
$carForTypicalCustomer = $carFactory();
$carForVipCustomer = $carFactory(true);

现在,如您所见,我的工厂实现打破了 DI 原则:它不是从调用代码(如果我使用 DI 容器,来自 Injector)接收实例,而是自己创建它们。所以,我试图重写它,使它不违反 DI 原则。这是我得到的:

interface ICarSimpleFactoryForCarFactory
{
    public function __invoke(IEngine $engine): Car;
}

interface IEngineSimpleFactoryForCarFactory
{
    public function __invoke(): IEngine;
}

class CarFactory
{
    private $carSimpleFactory;
    private $regularEngineSimpleFactory;
    private $advancedEngineSimpleFactory;

    public function __construct(
        ICarSimpleFactoryForCarFactory $carSimpleFactory,
        IEngineSimpleFactoryForCarFactory $regularEngineSimpleFactory,
        IEngineSimpleFactoryForCarFactory $advancedEngineSimpleFactory
    )
    {
        $this->carSimpleFactory = $carSimpleFactory;
        $this->regularEngineSimpleFactory = $regularEngineSimpleFactory;
        $this->advancedEngineSimpleFactory = $advancedEngineSimpleFactory;
    }

    public function __invoke(bool $isForVipCustomer = false): Car
    {
        $engine = ($isForVipCustomer ? $this->advancedEngineSimpleFactory :
            $this->regularEngineSimpleFactory)();
        return ($this->carSimpleFactory)($engine);
    }
}

// And here is my composition root:
// (sinse I'm now using DI, I need to inject some dependencies here)
$carFactory = new CarFactory(
    new class implements ICarSimpleFactoryForCarFactory {
        public function __invoke(IEngine $engine): Car
        {
            return new Car($engine);
        }
    },
    new class implements IEngineSimpleFactoryForCarFactory {
        public function __invoke(): IEngine
        {
            return new RegularEngine;
        }
    },
    new class implements IEngineSimpleFactoryForCarFactory {
        public function __invoke(): IEngine
        {
            return new AdvancedEngine;
        }
    }
);
$carForTypicalCustomer = $carFactory();
$carForVipCustomer = $carFactory(true);

我不得不介绍 2 个小型工厂的新接口,它们的唯一职责是让 Dependency Injector 有机会向它们注入实例,并分别向主 CarFactory 注入实例。 (如果我使用 DI 容器。)

在我看来,这样的解决方案过于复杂并且违反了 KISS 设计原则:只是为了将“new”运算符的使用从我的类转移到组合根,我必须创建一个接口及其实现,只是为了让注射器做它的工作?

那么,遵循 DI 模式实现工厂的正确方法是什么? 或者,我提供的解决方案是这个问题的唯一正确答案?

【问题讨论】:

    标签: php design-patterns dependency-injection factory


    【解决方案1】:

    我认为整个讨论取决于您在应用程序中的哪个位置需要您的汽车、频率以及种类。

    首先要做的事情:您的初始工厂已经很糟糕了。使用 __invoke() 并不能真正帮助使调用此方法的代码清晰,并且使用布尔参数在仅有的两个实现之间切换可能违反其他良好原则,因为您将此工厂的结果限制为两种可能的结果。

    但除此之外,基本问题是:您的应用程序需要多少辆汽车,以及何时需要。以对数据库连接类执行完全相同的示例:在任何给定时间,您可能只需要一种类型的数据库来完成一项任务,因此封装查询数据库代码的类将需要数据库接口的实例在它的构造函数中,就像你的 IEngine 接口一样。

    因为允许高级数据库访问的对象被认为是一个长期对象(它最终将被创建一次,然后根据需要存在,并且仅在 PHP 脚本结束时被销毁),它的创建应该发生在组合根。如何?

    不需要实例化的工厂类!太矫枉过正了。回到您的汽车示例,在脚本的最后,您需要一个带有引擎的 car 实例,它应该是暂时需要她的唯一汽车。

    而不是写:

    $carFactory = new CarFactory;
    $carForTypicalCustomer = $carFactory();
    $carForVipCustomer = $carFactory(true);
    

    你也可以写

    $engine = new RegularEngine;
    $carForTypicalCustomer = new Car($engine);
    

    根本没有工厂。

    想象一下,您使用库而不是自己编程汽车。总是记住如何从该供应商处创建汽车会很乏味(也许您总是忘记将手册放在哪里),因此将创建汽车的代码放入函数甚至静态方法中会更方便工厂类的:

    class CarFactory {
        static public getInstance() {
            $engine = new RegularEngine;
            return new Car($engine);
        }
    }
    # and in your code
    $carForTypicalCustomer = CarFactory::getInstance();
    

    这将做所有微小细节的知识与只需要整个事情的实际地方分开。

    我跳过有关自定义上述工厂输出的讨论,因为添加布尔参数很容易,或者允许带有类名的字符串,或者允许 IEngine 接口的实例或任何东西,因为这无关紧要我的观点。

    让我们考虑另一种情况:您的应用程序在整个应用程序生命周期中都需要一个汽车对象,而是在某个地方必须大量生产新的汽车对象(试想一下尝试向不同的汽车对象发送电子邮件)收件人或类似的东西)。这样的汽车对象不属于组合根。另一个需要无限量汽车对象的类需要一个工厂实例来创建新车。而这个另一个对象属于组合根,它必须在那里注入 CarFactory。如何做到这一点:滚动回到这个答案的开头,这是一个递归过程。

    您的第二家汽车工厂的大量额外接口来自您开始创建依赖注入框架这一事实。也许您应该继续出于教育目的,但我认为这没有用。有非常好的和小的可用 DI 框架示例,其中一个甚至可以放入不到 140 个字符的推文中。来看看吧:http://twittee.org/

    class Container {
     protected $s=array();
     function __set($k, $c) { $this->s[$k]=$c; }
     function __get($k) { return $this->s[$k]($this); }
    }
    

    这就是完全正常工作的 DI 容器所需要的一切。里面会发生什么?您创建一个实例,然后向其添加“属性”,这将触发调用魔术__set 方法并在内部将参数保存到数组中。

    你必须分配闭包(并注意没有错误检查,所以不要在生产中使用它)。这对于 DB 连接参数或汽车颜色等标量值很简单。为了做一些有用的事情,您还可以分配接受一个参数的闭包,即 DI 容器本身,并且应该返回您想要的任何内容。

    让我们配置您的第一辆车示例:

    $c = new Container();
    $c->car = function ($c) {
        return new Car($c->engine);
    }
    $c->engine = function () {
        return new RegularEngine();
    }
    $carForTypicalCustomer = $c->car
    

    同样,这不考虑可配置性。该怎么做?

    $c = new Container();
    $c->typicalCar = function ($c) {
        return new Car($c->regularEngine);
    }
    $c->regularEngine = function () {
        return new RegularEngine();
    }
    $c->vipCar = function ($c) {
        return new Car($c->advancedEngine);
    }
    $c->advancedEngine = function () {
        return new AdvancedEngine();
    }
    $carForTypicalCustomer = $c->regularCar;
    $carForVipCustomer = $c->vipCar;
    

    我们做到了:这两种类型的汽车的建造计划都被移到了做正确事情的封闭中,您仍然需要决定是想要一种还是另一种类型的汽车 - 没有布尔参数不再,但我希望我给您的印象是,通常在应用程序中,您没有这样的参数影响组合根对象的构建过程。添加这样的参数存在不存在的问题。但如果你真的想要它怎么办?试想一下,汽车的类型来自配置文件什么的:

    $c = new Container();
    $c->car = function ($c) {
        return new Car($c->engine);
    }
    $c->engine = function ($c) {
        return ($c->engineType) ? new AdvancedEngine : new RegularEngine;
    }
    $c->engineType = function () { 
        return true; 
    }
    $carForTypicalCustomer = $c->car
    

    您可以添加从某处读取配置的任何代码,而不是使用固定的 return true 作为引擎类型。

    请注意,在我的示例的这种状态下,这个简短的 DI 容器已经达到了它的边缘情况。直接分配标量值或将它们封装在闭包中会更方便。更高级的 DI 容器具有此功能 - 例如查看 Pimple

    更高级的 DI 容器会查看请求实例化的类的代码,并尝试自行(或借助代码注释)找出其他依赖的类需要实例化。

    例如,PHP-DI 有一个非常好的自动装配功能,如果构造函数中的类型提示指向单个类,它将简单地尝试调用 new $typehint。在您的情况下,类型提示指向一个无法实例化的接口,您需要告诉 PHP-DI 您需要哪个实现。您必须选择 RegularEngine 或 AdvancedEngine。

    请注意,高级 DI 容器通常会将任何创建的对象视为具有单例行为,即它们只会在第一次请求时创建一个实例,并且在后续请求中将返回 SAME 实例。所以为引擎添加可配置性是没有用的——只要将 Car 对象用作长期对象,就完全可以了。

    如果你需要大量的新实例,可以在 DI 容器中添加一个工厂类,并将这个工厂注入到需要汽车实例的地方,这个工厂可能只提供一个简单的方法布尔参数并整天发出具有不同引擎的新汽车实例。但是,短期对象不属于组合根,因此不在依赖注入(作为一般概念)或 DI 容器的范围内——它们属于应用程序代码,也就是“业务逻辑”。如果 Car 对象和 Engines 属于同一个域,或者位于同一个包中,如果不需要任何更多的自定义,只需调用 new Car(new RegularEngine) 是完全有效的。大多数情况下,您不需要完美的可配置性,您只需要完成一项特定的工作。是时候只做那一项工作,除此之外什么都不做。 You ain't gonna need it! (YAGNI)

    【讨论】:

    • 第一件事。 “您的应用程序需要多少辆汽车,何时需要”我使用了工厂模式,因为我需要很多这些对象,而且都在运行时——您提出的第二个选项。
    • “只调用 new 是完全有效的”——这意味着每次调用 CarFactory 时,我都会得到带有真实引擎的真实汽车,无论我传递什么参数! — 这是直接依赖硬编码!这不是完全违反了DI原则吗?还是在工厂把它弄坏就可以了?我认为它只允许在组合根中,而在业务逻辑或模型类中是禁止的,不是吗?而且在这种情况下,正如你所说,CarFactory 不属于组合根……那么,为什么允许在这里破坏规则?这是 DI 追随者常用的做法吗?
    • Tweetie&Pimple。我宁愿避免使用它们,因为它们是简单的服务定位器,通常称为反模式。我更喜欢使用 PHP-DI 或 Auryn——它们允许真正的依赖注入和自动装配(你提到了后者)。但我的问题不是关于使用 DIC——而是关于工厂的正确实现,它必须在运行时创建具有其依赖关系的对象,并且应该同时遵循 DI 模式。如果我理解正确的话,这与使用具体的 DIC 无关:无论您是否使用其中任何一个 - 问题都是一样的。
    • “DI 容器通常会将任何创建的对象视为具有单例行为。”是的,通常情况下,但这可以配置。例如,PHP-DI 具有“范围”方法,其唯一目的是配置该行为。此外,这种行为可以在工厂中进行控制:可以将容器注入他的工厂并调用 $container->make 来创建新实例,或者调用 $container->get 根据其“范围”策略获取实例。
    • Pimple 以 PHP-DI 的方式实现container-interop-接口。我可以将两者都注入 Slim 3 App 对象作为唯一的构造函数参数。他们只需要正确配置。当然,PHP-DI 做了很多自动化操作,并且花费了大量精力来减少必要的手动配置,这是一件好事。另一方面,疙瘩需要手动配置任何东西。然而,这个简单的例子表明它们是相同的。如果您的业务代码依赖于任何容器,您将获得服务定位器反模式。避免它,它的罚款。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多