在设计系统时,我经常面临使其他模块使用大量模块(日志记录,数据库访问等)的问题。问题是,如何将这些组件提供给其他组件。有两个答案似乎可能是依赖注入或使用工厂模式。但是,两者似乎都错了:


功能使测试很麻烦,并且不允许轻松交换实现。它们也不会使依赖项变得明显(例如,您正在检查一个方法,而忽略了它调用的方法会调用使用数据库的方法的事实)。
依赖注入使构造函数参数大为膨胀列表,它会在代码的某些方面模糊不清。典型的情况是一半以上类的构造函数看起来像这样(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)


这是一种典型的情况,我有一个问题:
我有异常类,这些异常类使用加载的错误描述从数据库中,使用具有用户语言设置参数的查询,该查询位于用户会话对象中。因此,要创建一个新的异常,我需要一个描述,它需要一个数据库会话和一个用户会话。因此,我注定要在所有方法中拖动所有这些对象,以防万一我可能需要引发异常。

我该如何解决这个问题?

评论

如果工厂可以解决您所有的问题,也许您可​​以将工厂注入对象并从中获取LoggingProvider,DbSessionProvider,ExceptionFactory和UserSession。

方法的太多“输入”,无论是传递还是注入,都是方法设计本身的问题。无论您选择哪种方法,都可能希望稍微减小方法的大小(一旦注入就更容易做到)

这里的解决方案不应减少参数。取而代之的是建立可以构建更高级别对象的抽象,该对象可以执行对象中的所有工作并为您带来好处。

#1 楼

使用依赖注入,但是只要构造函数参数列表太大,就可以使用Facade Service对其进行重构。想法是将一些构造函数参数组合在一起,引入一个新的抽象。

例如,您可以引入一种新型SessionEnvironment,其中封装了DBSessionProviderUserSession和已加载的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>();


两个代码段都具有相同的目的,它们在IFooFoo之间建立了链接。其他所有内容都只是语法。在任何一个代码示例中,将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