功能使测试很麻烦,并且不允许轻松交换实现。它们也不会使依赖项变得明显(例如,您正在检查一个方法,而忽略了它调用的方法会调用使用数据库的方法的事实)。
依赖注入使构造函数参数大为膨胀列表,它会在代码的某些方面模糊不清。典型的情况是一半以上类的构造函数看起来像这样
(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)
这是一种典型的情况,我有一个问题:
我有异常类,这些异常类使用加载的错误描述从数据库中,使用具有用户语言设置参数的查询,该查询位于用户会话对象中。因此,要创建一个新的异常,我需要一个描述,它需要一个数据库会话和一个用户会话。因此,我注定要在所有方法中拖动所有这些对象,以防万一我可能需要引发异常。
我该如何解决这个问题?
#1 楼
使用依赖注入,但是只要构造函数参数列表太大,就可以使用Facade Service对其进行重构。想法是将一些构造函数参数组合在一起,引入一个新的抽象。例如,您可以引入一种新型
SessionEnvironment
,其中封装了DBSessionProvider
,UserSession
和已加载的Descriptions
。要知道哪种抽象最有意义,就必须知道程序的细节。在SO上已经问过类似的问题。
评论
+1:我认为将构造函数参数分组为类是一个很好的主意。它还会迫使您将这些参数组织成更多的含义结构。
–乔治
2013年3月11日13:51
但是,如果结果不是有意义的结构,那么您就是在掩盖违反SRP的复杂性。在这种情况下,应该进行类重构。
–johnlemon
2013年9月15日13:22
@ Giorgio我不同意“将构造函数参数分组为类是一个很好的主意”的一般说法。如果您用“在这种情况下”将其限定,那就不一样了。
– tymtam
16 Sep 16 '16 at 0:29
#2 楼
Dependecy注入使构造函数的参数列表大量膨胀,并在代码中覆盖了某些方面。
由此看来,您似乎并不了解DI是正确的-这个想法是反转工厂内部的对象实例化模式。
您的特定问题似乎是一个更一般的OOP问题。为什么对象不能在运行时只抛出普通的,人类无法理解的异常,然后在最终try / catch之前有一些捕获该异常的东西,然后使用会话信息抛出新的,更漂亮的异常
另一种方法是拥有一个异常工厂,该异常工厂通过其构造函数传递给对象。该类可以引发工厂方法,而不是引发新异常(例如
throw PrettyExceptionFactory.createException(data)
。请记住,除工厂对象外,您的对象永远不要使用new
运算符。通常是一种特殊情况,但在您的情况下,可能是一个例外!评论
我在某处读到,当您的参数列表过长时,这不是因为您使用的是依赖注入,而是因为您需要更多的依赖注入。
–乔治
2013年3月11日13:52
这可能是原因之一-通常,根据语言的不同,您的构造函数应具有不超过6-8个参数,并且对象本身应不超过3-4个参数,除非使用特定模式(例如Builder模式) )命令。如果由于对象实例化其他对象而将参数传递给构造函数,那么IoC就是一个明显的例子。
–乔纳森·里奇(Jonathan Rich)
2013年3月11日13:55
#3 楼
您已经很好地列出了静态工厂模式的缺点,但是我不太同意依赖注入模式的缺点:依赖注入要求您为每个依赖写代码。这不是错误,而是一个功能:它迫使您考虑是否真的需要这些依赖项,从而促进松散耦合。在您的示例中:
这是我遇到的典型情况:我有异常类,这些异常类使用从数据库加载的错误描述,并使用带有用户语言设置参数的查询,位于用户会话对象中。因此,要创建一个新的异常,我需要一个描述,它需要一个数据库会话和一个用户会话。因此,我注定要在所有方法中拖动所有这些对象,以防万一我可能需要引发异常。
不,你没有被注定。为什么业务逻辑负责为特定用户会话本地化错误消息?如果将来某个时候您想通过批处理程序使用该业务服务(该程序没有用户会话...),该怎么办?或者,如果错误消息不应显示给当前登录的用户,而是显示其主管(可能更喜欢另一种语言),该怎么办?或者,如果您想在客户端上重用业务逻辑(没有访问数据库的权限...)?
很显然,对消息进行本地化取决于谁看这些消息,即表示层的责任。因此,我会从业务服务中抛出普通的异常,这些异常恰好带有消息标识符,然后可以在碰巧使用的任何消息源中查找表示层的异常处理程序。
这样,您可以删除3个不必要的依赖项(UserSession,ExceptionFactory和可能的描述),从而使您的代码更简单,更通用。
一般来说,我只将静态工厂用于您需要普遍访问的事物,并且保证可以在我们可能希望运行代码的所有环境中使用(例如日志记录)。对于其他所有内容,我将使用普通的旧式依赖项注入。
评论
这个。我看不到需要打数据库引发异常。
– Caleth
18年7月31日在12:39
#4 楼
使用依赖注入。使用静态工厂是Service Locator
反模式的一种。在此处查看Martin Fowler的开创性工作-http://martinfowler.com/articles/injection.html 如果构造函数参数太大而您不打算使用DI容器,请编写您自己的工厂进行实例化,从而可以通过XML或将实现绑定到接口来对其进行配置。
评论
服务定位器不是反模式-Fowler自己在您发布的URL中引用了它。尽管可以滥用服务定位器模式(就像滥用Singletons一样,以抽象出全局状态),但这是一个非常有用的模式。
–乔纳森·里奇(Jonathan Rich)
2013年3月11日13:37
有趣的知道。我一直听说它被称为反模式。
–山姆
2013年3月11日13:45
如果服务定位器用于存储全局状态,那么这只是一个反模式。服务定位器在实例化之后应该是无状态的对象,并且最好是不可变的。
–乔纳森·里奇(Jonathan Rich)
2013年3月11日13:57
XML不是类型安全的。在其他所有方法均告失败之后,我将其视为计划z
–Richard Tingle
15-10-18在16:41
#5 楼
我也可以使用依赖注入。请记住,DI不仅通过构造函数完成,而且还通过属性设置程序完成。例如,可以将记录器作为属性注入。此外,您可能希望使用IoC容器来减轻您的负担,例如通过将构造函数参数保留在域逻辑在运行时是必需的(保持构造函数以揭示类的意图和实际域依赖关系的方式),并可能通过属性注入其他帮助程序类。
您可以进一步想去的是面向方面的程序,它在许多主要框架中都已实现。这可以让您拦截(或“建议”使用AspectJ术语)类的构造函数并注入相关的属性,也许会赋予特殊的属性。
评论
我避免通过设置器进行DI,因为它引入了一个时间窗,在此期间内对象未处于完全初始化状态(在构造函数和设置器调用之间)。换句话说,它引入了一个方法调用顺序(必须在Y之前调用X),如果可能的话,我会避免使用它。
– RokL
2014年1月10日13:05
通过属性设置器进行DI是可选依赖项的理想选择。记录是一个很好的例子。如果需要记录,则设置Logger属性,如果不需要,则不要设置它。
–普雷斯顿
2014年2月19日在17:58
#6 楼
工厂使测试变得很痛苦,并且不允许轻松交换实现。它们也不会使依赖关系变得明显(例如,您正在检查一个方法,而忽略了该方法调用了调用使用数据库的方法的方法)。
我不太同意。至少不是一般情况。
简单工厂:
public IFoo GetIFoo()
{
return new Foo();
}
简单注入:
myDependencyInjector.Bind<IFoo>().To<Foo>();
两个代码段都具有相同的目的,它们在
IFoo
和Foo
之间建立了链接。其他所有内容都只是语法。在任何一个代码示例中,将Foo
更改为ADifferentFoo
都需要付出同样的努力。我听说人们认为依赖注入允许使用不同的绑定,但是关于制造不同的工厂,可以有相同的论点。选择正确的绑定与选择正确的工厂完全一样复杂。
工厂方法确实允许您例如在某些地方使用
Foo
,在其他地方使用ADifferentFoo
。有些人可能会称其为好(如果需要,则有用),有些人可能会称其为坏(您可以在更换所有东西时做些半事半功)。如果坚持下去,避免这种歧义并不难。到返回
IFoo
的单个方法,以便您始终只有一个源。如果您不想用脚射击,请不要握住重物,或者确保不要将脚对准脚。依赖注入使构造函数的参数列表大量膨胀,并在代码的所有部分蒙上阴影。
这就是为什么有些人喜欢在构造函数中显式检索依赖项的原因,例如:
public MyService()
{
_myFoo = DependencyFramework.Get<IFoo>();
}
我听说过参数pro(没有构造函数膨胀),听说过参数con(使用构造函数可以实现更多的DI自动化)。
就我个人而言,虽然我屈服于想要使用构造函数参数的上级,但我注意到VS中的下拉列表存在问题(右上方,用于浏览当前类的方法),其中当一个方法签名的长度比我的屏幕长(=> constructor肿的构造函数)。
从技术上来说,我都不在乎。两种选择都需要花费大量的精力来键入。而且由于您使用的是DI,因此您通常不会手动调用构造函数。但是Visual Studio UI错误确实使我更喜欢不要夸大构造函数参数。
作为副注,依赖项注入和工厂不是互斥的。在某些情况下,我插入了一个生成依赖关系的工厂,而不是插入依赖关系(幸运的是,NInject允许您使用
Func<IFoo>
,因此您无需创建实际的工厂类)。 这种用例很少见,但确实存在。
评论
OP询问静态工厂。 stackoverflow.com/questions/929021/…
–巴西
18年7月31日在13:41
@Basilevs“问题是,我该如何将这些组件提供给其他组件。”
– Anthony Rutledge
18年7月31日在14:10
@Basilevs除了旁注,我所说的所有内容也适用于静态工厂。我不确定您要具体指出什么。参考链接是什么?
–更
18年7月31日在16:11
这样一种注入工厂的用例可以是这样一种情况吗?有人设计了一个抽象的HTTP请求类,并制定了其他五种多态子类的策略,一个子类用于GET,POST,PUT,PATCH和DELETE?当尝试将所述类层次结构与MVC类型的路由器(可能部分依赖于HTTP请求类)进行集成时,您无法知道每次将使用哪种HTTP方法。PSR-7HTTP消息接口很丑陋。
– Anthony Rutledge
19年2月21日在4:27
@AnthonyRutledge:DI工厂可能意味着两件事(1)一个公开多个方法的类。我想这就是你在说的。但是,这并不是工厂特有的。无论是具有多个公共方法的业务逻辑类,还是具有多个公共方法的工厂,都是语义问题,并且没有技术差异。 (2)工厂特定于DI的用例是,非工厂依赖关系被实例化一次(在注入过程中),而工厂版本可用于在以后的阶段(甚至可能多次)实例化实际依赖关系。
–更
19年2月21日在7:35
#7 楼
在此模拟示例中,工厂类在运行时用于根据HTTP请求方法确定要实例化的入站HTTP请求对象的类型。向工厂本身注入依赖项注入容器的实例。这使工厂可以确定运行时,并让依赖项注入容器处理依赖项。每个入站HTTP请求对象至少具有四个依赖关系(超全局变量和其他对象)。<?php
namespace TFWD\Factories;
/**
* A class responsible for instantiating
* InboundHttpRequest objects (PHP 7.x)
*
* @author Anthony E. Rutledge
* @version 2.0
*/
class InboundHttpRequestFactory
{
private const GET = 'GET';
private const POST = 'POST';
private const PUT = 'PUT';
private const PATCH = 'PATCH';
private const DELETE = 'DELETE';
private static $di;
private static $method;
// public function __construct(Injector $di, Validator $httpRequestValidator)
// {
// $this->di = $di;
// $this->method = $httpRequestValidator->getMethod();
// }
public static function setInjector(Injector $di)
{
self::$di = $di;
}
public static setMethod(string $method)
{
self::$method = $method;
}
public static function getRequest()
{
if (self::$method == self::GET) {
return self::$di->get('InboundGetHttpRequest');
} elseif ((self::$method == self::POST) && empty($_FILES)) {
return self::$di->get('InboundPostHttpRequest');
} elseif (self::$method == self::POST) {
return self::$di->get('InboundFilePostHttpRequest');
} elseif (self::$method == self::PUT) {
return self::$di->get('InboundPutHttpRequest');
} elseif (self::$method == self::PATCH) {
return self::$di->get('InboundPatchHttpRequest');
} elseif (self::$method == self::DELETE) {
return self::$di->get('InboundDeleteHttpRequest');
} else {
throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
}
}
}
在集中的
index.php
内,用于MVC类型设置的客户端代码可能看起来类似于以下内容(省略验证)。InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router'); // The Router class depends on InboundHttpRequest objects.
$router->dispatch();
或者,您可以删除工厂的静态性质(和关键字),并允许依赖项注入器来管理整个事情(因此,注释掉的构造函数)。但是,您必须将类成员引用(
self
)的某些(而不是常量)更改为实例成员($this
)。评论
没有评论的投票没有用。
– Anthony Rutledge
18年7月31日在18:46
评论
如果工厂可以解决您所有的问题,也许您可以将工厂注入对象并从中获取LoggingProvider,DbSessionProvider,ExceptionFactory和UserSession。方法的太多“输入”,无论是传递还是注入,都是方法设计本身的问题。无论您选择哪种方法,都可能希望稍微减小方法的大小(一旦注入就更容易做到)
这里的解决方案不应减少参数。取而代之的是建立可以构建更高级别对象的抽象,该对象可以执行对象中的所有工作并为您带来好处。