我应该使用依赖注入还是静态工厂?


81

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

  • 工厂使测试变得痛苦,并且不允许轻易交换实现。它们也不会使依赖关系变得明显(例如,您正在检查一个方法,而忽略了它调用的方法会调用使用数据库的方法的事实)。
  • Dependecy注入使构造函数的参数列表大量膨胀,并且在代码的所有部分涂上了污点。典型情况是一半以上类的构造函数如下所示(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

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

我该如何解决这个问题?


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

1
方法的太多“输入”,无论是通过还是注入,都是方法设计本身的问题。无论您采用哪种方法,都可能希望稍微减小方法的大小(一旦完成注射就更容易做到)
Bill K

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

Answers:


74

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

例如,您可以引入一个SessionEnvironment封装了DBSessionProviderUserSession和和的新类型Descriptions。要知道哪种抽象最有意义,但是,人们必须知道程序的细节。

关于SO已经在这里提出类似的问题。


9
+1:我认为将构造函数参数分组为类是一个很好的主意。它还会迫使您将这些参数组织成更多的含义结构。
乔治

5
但是,如果结果不是有意义的结构,那么您就是在掩盖违反SRP的复杂性。在这种情况下,应该进行类重构。
danidacar 2013年

1
@ Giorgio我不同意“将构造函数参数分组为类是一个很好的主意”的一般说法。如果您用“在这种情况下”将其限定,那就不一样了。
tymtam '16

19

Dependecy注入使构造函数的参数列表大量膨胀,并且在代码的所有部分涂上了污点。

由此看来,您似乎不太了解DI-想法是在工厂内部反转对象实例化模式。

您的特定问题似乎是一个更一般的OOP问题。为什么对象不能在运行时抛出普通的,人类无法理解的异常,然后在最终try / catch之前有一些捕获该异常的东西,然后使用会话信息抛出新的,更漂亮的异常?

另一种方法是拥有一个异常工厂,该异常工厂通过其构造函数传递给对象。该类可以引发工厂的方法(例如,而不是引发新的异常)throw PrettyExceptionFactory.createException(data)

请记住,除了工厂对象外,您的对象永远不要使用new操作员。通常,例外是一种特殊情况,但在您的情况下,它们可能是例外!


1
我在某处读到,当您的参数列表过长时,这不是因为您使用的是依赖注入,而是因为您需要更多的依赖注入。
Giorgio 2013年

这可能是原因之一-通常,根据语言的不同,您的构造函数应具有不超过6-8个参数,并且对象本身应不超过3-4个参数,除非使用特定的模式(如Builder模式)决定它。如果您因为对象实例化其他对象而将参数传递给构造函数,那么IoC就是一个明显的例子。
乔纳森·里奇

12

您已经很好地列出了静态工厂模式的缺点,但是我不太同意依赖注入模式的缺点:

依赖关系注入要求您为每个依赖关系编写代码不是一个错误,而是一个功能:它迫使您考虑是否真的需要这些依赖关系,从而促进松散耦合。在您的示例中:

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

不,你没有注定。为什么业务逻辑负责为特定用户会话本地化错误消息?如果将来某个时候您想通过批处理程序使用该业务服务(该程序没有用户会话...),该怎么办?或者,如果错误消息不应显示给当前登录的用户,而是显示其主管(可能希望使用其他语言),该怎么办?或者,如果您想在客户端上重用业务逻辑(该客户端无权访问数据库...),该怎么办?

显然,对消息进行本地化取决于谁看这些消息,即表示层负责。因此,我会从业务服务中抛出普通的异常,该异常恰好带有消息标识符,然后可以在碰巧使用的任何消息源中查找表示层的异常处理程序。

这样,您可以删除3个不必要的依赖项(UserSession,ExceptionFactory以及可能的描述),从而使您的代码既简单又通用。

一般来说,我只将静态工厂用于您需要普遍访问的事物,并且可以保证在我们可能要运行代码的所有环境中都可以使用(例如日志记录)。对于其他所有内容,我将使用普通的旧式依赖项注入。


这个。我看不到需要打数据库引发异常。
卡雷斯(Caleth)'18

1

使用依赖注入。使用静态工厂是Service Locator反模式的一种运用。在此处查看Martin Fowler的开创性工作-http: //martinfowler.com/articles/injection.html

如果构造函数的参数太大,并且您不使用DI容器,请编写自己的工厂进行实例化,以使其可配置(通过XML或将实现绑定到接口)。


5
服务定位器不是反模式-Fowler自己在您发布的URL中引用了它。尽管可以滥用Service Locator模式(以滥用Singletons的方式-抽象出全局状态),但这是一个非常有用的模式。
乔纳森·里奇

1
有趣的知道。我一直听说它被称为反模式。
2013年

4
如果服务定位器用于存储全局状态,则这只是一个反模式。服务定位器在实例化之后应该是无状态的对象,并且最好是不可变的。
乔纳森·里奇

XML不是类型安全的。在其他所有方法均

1

我也可以使用依赖注入。请记住,DI不仅通过构造函数完成,而且还通过属性设置程序完成。例如,记录器可以作为属性注入。

此外,您可能想使用一个IoC容器,这可能会减轻您的负担,例如,通过将构造函数参数保留在域逻辑在运行时所需的东西(保持构造函数以某种方式揭示其意图)。类和实际域依赖项),并可能通过属性注入其他帮助程序类。

您可能想走的更远一步是面向方面的程序,它在许多主要框架中实现。这可以让您拦截(或“建议”使用AspectJ术语)类的构造函数并注入相关的属性,也许会给定特殊的属性。


4
我避免通过设置器进行DI,因为它引入了一个时间窗,在此期间,对象未处于完全初始化状态(在构造函数和设置器调用之间)。换句话说,它引入了一个方法调用顺序(必须在Y之前调用X),如果可能的话,我会避免使用它。
RokL 2014年

1
通过属性设置器进行DI是可选依赖项的理想选择。记录是一个很好的例子。如果需要记录,则设置Logger属性,如果不需要,则不要设置它。
普雷斯顿

1

工厂使测试变得痛苦,并且不允许轻易交换实现。它们也不会使依赖关系变得明显(例如,您正在检查一个方法,而忽略了它调用的方法会调用使用数据库的方法的事实)。

我不太同意。至少不是一般。

简单工厂:

public IFoo GetIFoo()
{
    return new Foo();
}

简单注射:

myDependencyInjector.Bind<IFoo>().To<Foo>();

这两个代码段的目的相同,它们在IFoo和之间建立了链接Foo。其他所有内容只是语法。

在任何一个代码示例中,更改Foo为都ADifferentFoo花费了同样多的精力。
我听说有人争辩说依赖注入允许使用不同的绑定,但是对于制造不同的工厂可以提出相同的观点。选择正确的绑定与选择正确的工厂一样复杂。

工厂方法确实允许您Foo在某些地方和ADifferentFoo其他地方使用。有些人可能称其为好(如果需要,则有用),有些人则称其为坏(您可以在更换所有东西时做些半定的工作)。
但是,如果您坚持使用单一的返回方法,IFoo以便始终拥有单一的来源,那么避免这种歧义并不是很困难。如果您不想用脚射击,请不要握住装有枪支的枪,或确保不要将其对准脚。


Dependecy注入使构造函数的参数列表大量膨胀,并且在代码的所有部分涂上了污点。

这就是为什么有些人喜欢在构造函数中显式检索依赖项的原因,如下所示:

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

我听说过参数pro(没有构造函数膨胀),也听说过参数con(使用构造函数可以实现更多的DI自动化)。

就我个人而言,虽然我屈服于想要使用构造函数参数的上级,但我注意到VS中的下拉列表存在问题(右上方,用于浏览当前类的方法),其中当一个方法签名的长度比我的屏幕长(=> constructor肿的构造函数)。

从技术上讲,我不在乎这两种方式。两种选择都需要花费很多精力来键入。而且由于您使用的是DI,因此您通常不会手动调用构造函数。但是Visual Studio UI错误确实使我赞成不要not肿构造函数参数。


附带说明一下,依赖项注入和工厂不是互斥的。在某些情况下,我没有插入依赖项,而是插入了一个生成依赖项的工厂(幸运的是,NInject允许您使用它,Func<IFoo>因此您无需创建实际的工厂类)。

用例很少见,但确实存在。



@Basilevs“问题是,我该如何将这些组件提供给其他组件。”
安东尼·鲁特里奇

1
@Basilevs除了旁注,我说的所有内容也适用于静态工厂。我不确定您要具体指出什么。参考链接是什么?
平坦

这样一种注入工厂的用例可以是这样一种情况吗?有人设计了一个抽象的HTTP请求类,并计划了另外五个多态子类,一个子类用于GET,POST,PUT,PATCH和DELETE?你可以不知道哪个HTTP方法将试图说的类层次结构与MVC类型的路由器(可以部分依赖于HTTP请求类整合时,每次使用PSR-7 HTTP消息接口是丑陋的。
安东尼拉特利奇

@AnthonyRutledge:DI工厂可能意味着两件事(1)一个公开多个方法的类。我想这就是你在说的。但是,这并不是工厂特有的。无论是具有多个公共方法的业务逻辑类,还是具有多个公共方法的工厂,都是语义问题,并且没有技术差异。(2)工厂特定于DI的用例是,非工厂依赖关系被实例化一次(在注入过程中),而工厂版本可用于在以后的阶段(甚至可能多次)实例化实际依赖关系。
更加平坦

0

在此模拟示例中,工厂类在运行时用于基于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.");
        }
    }
}

在集中化的范围内,用于MVC类型设置的客户端代码index.php可能类似于以下内容(省略了验证)。

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)。


没有评论的投票没有用。
安东尼·鲁特里奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.