Ana*_*nov 1 php design-patterns factory dependency-injection
我是 DI 模式的新手,因此,这是我的问题。
如果我有某个工厂实现了一些业务逻辑,并在运行时根据给定的输入参数创建了它的主要对象及其依赖项,我应该如何实现它才能不破坏 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);
Run Code Online (Sandbox Code Playgroud)
现在,如您所见,我的工厂实现打破了 DI 原则:它不是从调用代码(来自 Injector,如果我使用 DI 容器)接收实例,而是自己创建它们。所以,我试图重写它,这样它就不会破坏 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);
Run Code Online (Sandbox Code Playgroud)
我不得不引入 2 个小工厂的新接口,它们的唯一职责是让依赖注入器有机会向它们注入实例,分别向主 CarFactory 注入实例。(如果我使用 DI 容器。)
在我看来,这样的解决方案过于复杂并打破了 KISS 的设计原则:只是为了将“new”操作符的使用从我的类转移到组合根,我不得不创建一个接口及其实现,只是为了让注入器做它的工作吗?
那么,遵循 DI 模式实施工厂的正确方法是什么?或者,我提供的解决方案是这个问题的唯一正确答案?
我认为整个讨论取决于您的应用程序中您需要汽车的位置、频率和种类。
第一件事:你最初的工厂已经很糟糕了。使用 __invoke() 并不能真正帮助使调用此方法的代码清晰,并且使用布尔参数在仅有的两个实现之间切换可能违反了其他良好原则,因为您将此工厂的结果限制为两种可能的结果。
但除此之外,最重要的问题是:您的应用程序需要多少辆汽车,以及何时需要。以对数据库连接类做完全相同的示例为例:在任何给定时间,您可能只需要一种类型的数据库来完成一项任务,因此封装代码以查询数据库的类将需要数据库接口的实例在它的构造函数中就像你的 IEngine 接口一样。
因为允许高级数据库访问的对象被认为是一个长期对象(它最终会被创建一次,然后根据需要存活多久,并且只有在 PHP 脚本结束时才会被销毁),所以它的创建应该发生在组合根. 如何?
不需要实例化的工厂类!太矫情了 回到您的汽车示例,在脚本的末尾,您需要一个car
带有引擎的实例,它应该是目前唯一需要她的汽车。
而不是写:
$carFactory = new CarFactory;
$carForTypicalCustomer = $carFactory();
$carForVipCustomer = $carFactory(true);
Run Code Online (Sandbox Code Playgroud)
你也可以写
$engine = new RegularEngine;
$carForTypicalCustomer = new Car($engine);
Run Code Online (Sandbox Code Playgroud)
根本没有工厂。
想象一下,您不是自己编程汽车,而是使用库。总是记住如何从这个供应商那里创建汽车是很乏味的(也许你总是忘记你把手册放在哪里),所以将创建汽车的代码放入函数中更方便 - 甚至放入静态方法工厂类:
class CarFactory {
static public getInstance() {
$engine = new RegularEngine;
return new Car($engine);
}
}
# and in your code
$carForTypicalCustomer = CarFactory::getInstance();
Run Code Online (Sandbox Code Playgroud)
这将完成所有微小细节的知识与需要整个事情的实际位置分开。
我跳过了关于自定义上述工厂输出的讨论,因为添加布尔参数很容易,或者允许带有类名的字符串,或者允许 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); }
}
Run Code Online (Sandbox Code Playgroud)
这就是一个完全工作的 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
Run Code Online (Sandbox Code Playgroud)
同样,这不考虑可配置性。怎么做?
$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;
Run Code Online (Sandbox Code Playgroud)
我们是这样的:这两种类型的汽车的建造计划被移到了做正确事情的封闭区中,你仍然需要决定你想要一种还是另一种类型的汽车——不再有布尔参数,但是我希望我给你的印象是,通常在应用程序中,你没有这样一个影响组合根对象构建过程的参数。添加这样的参数有一个不存在的问题。但如果你真的想要它呢?想象一下汽车的类型来自配置文件或其他东西:
$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
Run Code Online (Sandbox Code Playgroud)
return true
您可以添加从某处读取配置的任何代码,而不是使用固定的引擎类型。
请注意,在我的示例的这种状态下,这个简短的 DI 容器已经进入了它的边缘情况。直接分配标量值或者必须将它们封装在闭包中会更方便。更高级的 DI 容器具有此功能 - 例如查看Pimple。
甚至更高级的 DI 容器会查看请求实例化的类的代码,并尝试自己(或借助代码注释)找出其他依赖的类需要实例化。
例如,PHP-DI有一个非常好的自动装配功能,new $typehint
如果构造函数中的类型提示指向单个类,它只会尝试调用。在您的情况下,typehint 指向一个无法实例化的接口,您需要告诉 PHP-DI 您需要哪个实现。您必须选择RegularEngine 或AdvancedEngine。
请注意,高级 DI 容器通常会将任何创建的对象视为具有单例行为,即它们只会在第一次请求时创建一个实例,并且在后续请求中将返回 SAME 实例。因此,为引擎添加可配置性是没有用的——只要 Car 对象用作长期对象,就完全没问题。
然而,如果你需要大量的新实例,你可以在 DI 容器中添加一个工厂类,并在需要汽车实例的地方注入这个工厂,这个工厂可能只提供一个带有布尔参数的简单方法,并发出不同的新汽车实例引擎整天。但是,短期对象不属于组合根,因此不在依赖注入(作为一般概念)或 DI 容器的范围内——它们属于应用程序代码,也就是“业务逻辑”。如果 Car 对象和 Engines 属于同一个域,或者位于同一个包中,那么调用它是完全有效的new Car(new RegularEngine)
如果不需要任何更多的定制。大多数情况下,您不需要对所有事情都具有完美的可配置性,您只需要完成一项特定的工作。那是只做那一项工作的时候,除此之外别无他法。你不会需要它!(亚格尼)