我是DI模式的新手,因此,这是我的问题。
如果我有某个工厂实现了一些业务逻辑并且在运行时创建了它的主要对象及其依赖项,根据给定的输入参数,我应该如何实现它以便它不会破坏DI原则? (它应该避免直接调用new运算符来实例化它的主要对象(由这个工厂生成)及其依赖关系,不应该吗?)
(编辑结束11 apr)
考虑以下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原则:不是从调用代码接收实例(来自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);
我不得不引入2个小工厂的新接口,它们的唯一责任是让依赖注入器向它们注入实例,并分别向主要CarFactory注入实例。 (如果我使用DI容器。)
在我看来,这样的解决方案过于复杂并打破了KISS设计原则:只是将“new”运算符的使用从我的类转移到组合根,我不得不创建一个接口及其实现,只是为了让喷射器完成它的工作?
那么,遵循DI模式,实施工厂的正确方法是什么? 或者,我提供的解决方案是这个问题的唯一正确答案?
答案 0 :(得分:4)
我认为整个讨论取决于您的应用程序在哪里需要您的汽车,频率和种类。
首先要做的事情:你的初始工厂已经很糟糕了。使用__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接口的实例或任何东西,因为&# 39;与我的观点无关。
让我们考虑一下不同的情况:在整个应用程序生命周期中,您的应用程序需要一个汽车对象,它有一个特定的地方,必须大量生产新的汽车对象(只想到尝试发送电子邮件给不同的收件人或类似的东西)。这样的汽车对象不属于组合根。需要无限量汽车对象的另一类需要工厂实例来创建新车。而这个其他对象属于组合根,并且必须注入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具有非常好的自动装配功能,如果构造函数中的typehint指向单个类,则只会尝试调用new $typehint
。在您的情况下,typehint指向一个无法实例化的接口,您需要告诉PHP-DI您需要哪种实现。您必须选择RegularEngine或AdvancedEngine。
请注意,高级DI容器通常会将任何创建的对象视为具有单例行为,即它们仅在首次请求时才创建一个实例,并且在后续请求中将返回SAME实例。因此,为引擎添加可配置性毫无用处 - 只要将Car对象用作长期对象,这就完全可以了。
如果您需要大量新实例,可以将工厂类添加到DI容器中,并将此工厂注入需要汽车实例的地方,而这个工厂可能只提供一个简单方法布尔参数,并且整天发出具有不同引擎的新汽车实例。但是,短期对象不属于组合根目录,因此不属于依赖注入(作为一般概念)或DI容器的范围 - 它们属于应用程序代码,又称"业务逻辑&#34 ;.如果Car对象和Engines属于同一个域,或者位于同一个包中,如果不需要进行任何自定义,则只需调用new Car(new RegularEngine)
即可。大多数情况下,你并不需要完美的可配置性,你只需要完成一项特定的工作。那是时候只做一件工作,而且没有其他任何工作。 You ain't gonna need it! (YAGNI)