依赖注入:场注入与构造函数注入?


61

我知道这是一个热门辩论,关于最佳做法的观点会随着时间而改变。

我曾经在课堂上专门使用字段注入,直到开始在不同的博客(例如petrikainulainenschauderhaftfowler)上阅读有关构造函数注入的好处的信息。从那以后,我切换了方法,将构造函数注入用于必需的依赖项,将setter注入用于可选的依赖项。

但是,我最近与JMockit的作者(一个模拟框架)进行了辩论,在其中他认为构造函数和setter注入是不良做法,并表示JEE社区同意他的观点。

在当今世界上,是否有首选的注射方法?现场注入是首选吗?

在过去的几年中,从字段注入转换为构造函数注入后,我发现使用起来更加清晰了,但是我想知道是否应该重新审视我的观点。JMockit的作者(RogérioLiesenfeld)显然精通DI,因此鉴于他强烈反对构造函数/ setter注入,我感到有义务重新审视我的方法。



2
如果不允许使用构造函数注入 setter注入,那么应该如何将依赖关系传递给类?也许您最好与辩论联系起来,除非您与Mockit的谈话是私人的。
罗伯特·哈维

2
@DavidArno:在一个不可变的类中,状态在构造函数中
罗伯特·哈维

1
@Davkd您所描述的是贫血领域模型。它当然有支持者,但它不再是面向对象的了。数据和作用于该数据的功能一起生活在哪里。
理查德·廷格

3
@David函数式编程没有错。但是Java从根本上被设计为面向对象的,如果您不太谨慎的话,您将不断与之抗争并最终陷入贫血领域模型。面向函数的代码没有什么问题,但应该使用适当的工具,而Java则不是(即使lambda已添加了基于函数的编程元素)
Richard Tingle 2015年

Answers:


48

就我的口味而言,野外注射有点“遥不可及”

考虑一下您在Google网上论坛帖子中提供的示例:

public class VeracodeServiceImplTest {


    @Tested(fullyInitialized=true)
    VeracodeServiceImpl veracodeService;
    @Tested(fullyInitialized=true, availableDuringSetup=true)
    VeracodeRepositoryImpl veracodeRepository;
    @Injectable private ResultsAPIWrapper resultsApiWrapper;
    @Injectable private AdminAPIWrapper adminApiWrapper;
    @Injectable private UploadAPIWrapper uploadApiWrapper;
    @Injectable private MitigationAPIWrapper mitigationApiWrapper;

    static { VeracodeRepositoryImpl.class.getName(); }
...
}

因此,基本上,您要说的是:“我有一个带有私有状态的类,并在其中附加了@injectable注释,这意味着即使我的状态已全部声明为私有,状态也可以由外部的某些代理自动填充

我了解这样做的动机。这是为了避免正确设置班级所固有的许多仪式。基本上,人们的意思是“我已经厌倦了编写所有这些样板,因此我将注释所有状态,然后让DI容器负责为我设置它。”

这是完全正确的观点。但这也是可以避免的语言功能的解决方法。另外,为什么要停在那里?传统上,DI依赖于每个具有配套接口的类。为什么不消除所有带有注解的接口呢?

考虑替代方案(它将是C#,因为我更了解它,但是Java中可能有一个完全等效的方案):

public class VeracodeService
{        
    private readonly IResultsAPIWrapper _resultsApiWrapper;
    private readonly IAdminAPIWrapper _adminApiWrapper;
    private readonly IUploadAPIWrapper _uploadApiWrapper;
    private readonly IMitigationAPIWrapper _mitigationApiWrapper;

    // Constructor
    public VeracodeService(IResultsAPIWrapper resultsApiWrapper, IAdminAPIWrapper adminApiWrapper, IUploadAPIWrapper uploadApiWrapper,         IMitigationAPIWrapper mitigationApiWrapper)
    {
         _resultsAPIWrapper = resultsAPIWrapper;
         _adminAPIWrapper = adminAPIWrapper;
         _uploadAPIWrapper = uploadAPIWrapper;
         _mitigationAPIWrapper = mitigationAPIWrapper;
    }
}

我已经知道了这堂课的一些知识。这是一门不变的课。状态只能在构造函数中设置(在这种情况下为引用)。而且由于所有内容都源自接口,因此我可以交换构造函数中的实现,这就是您的模拟输入的来源。

现在,我的DI容器所需要做的就是对构造函数进行反思,以确定需要更新哪些对象。但是,这种反思是以一流的方式对公众成员进行的。也就是说,元数据已经在构造函数中声明了该类的一部分,该方法的明确目的是为类提供所需的依赖项。

当然,这是很多样板,但这是语言的设计方式。注释似乎是对本应内置在语言本身中的东西的肮脏hack。


除非我看错了您的帖子,否则您实际上并没有正确地查看示例。我的实现VeracodeService几乎与您编写的实现相同(尽管在Java与C#中)。该VeracodeServiceImplTest实际上是一个单元测试类。@Injectable字段本质上是插入到上下文中的模拟对象。该@Tested字段是定义了DI构造函数的对象/类。我同意您的观点,即构造器注入胜于现场注入。但是,正如我提到的那样,JMockit作者感到相反,我试图理解原因
Eric B.

如果您查看该讨论中的第一篇文章,您会发现我的Repo类中的defn几乎完全相同。
Eric B.

如果您只是在谈论测试类,那就没关系了。因为,测试课。
罗伯特·哈维

不-我不是在谈论测试课程。我说的是生产课程。碰巧的是,讨论组中的示例是单元测试,因为该框架是模拟/测试框架。(整个讨论围绕模拟框架是否应支持构造函数注入)
Eric B.

3
+1:是的,在C#版本中没有魔术。这是一个标准类,它具有依赖项,在使用该类之前,它们都一次性注入。该类可以与DI容器一起使用,也可以不与DI容器一起使用。课堂上什么也没有说它是IOC或“自动依赖注入”框架。
Binary Worrier 2015年

27

较少测试初始化​​boiletplate的参数是有效的,但是还必须考虑其他一些问题。首先,您必须回答一个问题:

我是否希望我的课程只能通过反射实例化?

使用字段注入意味着将类的兼容性范围缩小到依赖注入环境,这些依赖注入环境使用反射实例化对象并支持这些特定的注入注释。某些基于Java语言的平台甚至不支持反射(GWT),因此,字段注入的类将与它们不兼容。

第二个问题是性能。构造函数调用(直接或通过反射)始终比一堆反射字段分配要快。依赖注入框架必须使用反射分析来构建依赖树并创建反射构造函数。此过程会导致其他性能下降。

性能会影响可维护性。如果每个测试套件必须在某种依赖项注入容器中运行,则数千个单元测试的测试运行可能会持续数十分钟。根据代码库的大小,这可能是一个问题。

所有这些都引发了许多新问题:

  1. 在另一个平台上使用部分代码的概率是多少?
  2. 将编写多少个单元测试?
  3. 准备每个新版本需要多快?
  4. ...

通常,项目越大,越重要,这些因素就越重要。同样,在质量方面,我们通常希望保持代码的兼容性,可测试性和可维护性。从哲学的角度来看,字段注入破坏了封装封装面向对象编程的四个基本原理之一,这是Java的主要范例。

许多反对电场注入的争论。


3
+1您很紧张。我不喜欢需要反射或DI / IoC框架来实例化一个类。这就像开车去罗马从阿姆斯特丹到达巴黎一样。介意你我爱DI。只是不确定框架。
Marjan Venema 2015年

1
很好的论点。它表达了我很多想法,但我很高兴看到其他人也有同样的想法。就像我以前发现场注入一样神奇,我认为我之所以如此喜欢它仅仅是因为它“更轻松”。我发现构造函数注入现在变得更加清晰。
Eric B.

19

现场注入得到我明确的“否”投票。

像罗伯特·哈维(Robert Harvey)一样,对于我的品味来说有点太神奇了。我更喜欢显式代码而不是隐式代码,并且仅当/提供了明显的好处时才容忍间接,因为它使代码更难以理解和推理。

像MaciejChałapuk一样,我不喜欢需要反射或DI / IoC框架来实例化一个类。这就像开车去罗马从阿姆斯特丹到达巴黎一样。

请记住,我爱DI及其带来的所有优势。只是不确定是否需要框架实例化一个类。特别是IoC容器或其他DI框架对测试代码不会产生影响。

我喜欢我的测试非常简单。我不喜欢通过间接设置IoC容器或其他DI框架来设置它们来测试我的类。感觉很尴尬。

它使我的测试依赖于框架的全局状态。也就是说,我不再能够并行运行两个需要将X类实例设置为具有不同依赖关系的测试。

至少使用构造函数和setter注入,您可以选择在测试中不使用框架和/或反射。


例如,如果使用Mockito mockito.org,则即使使用字段注入,也不需要DI / IoC框架,并且可以并行运行测试等等。
汉斯·彼得·斯托尔2015年

1
@hstoerr尼斯。但是,这必须意味着Mockito正在使用反射(或Java等效的反射)……
Marjan Venema 2015年

5

好吧,这似乎是一场争论不休的讨论。首先需要解决的是,场注入不同于塞特注入。我已经与一些认为Field和Setter注入是同一回事的人一起工作。

因此,要明确:

现场注入:

@Autowired
private SomeService someService;

二传手注射:

@Autowired
public void setSomeService(SomeService someService) {
    this.someService = someService;
}

但是,对我来说,他们也有同样的担忧。而且,我最喜欢的构造函数注入是:

@AutoWired
public MyService(SomeService someService) {
    this.someService = someService;
}

我有以下理由认为,构造函数注入优于setter / field注入(我将引用一些Spring链接来说明我的观点):

  1. 防止循环依赖:如@Tomasz所述,Spring能够检测Bean之间的循环依赖。另请参阅Spring Team这样说。
  2. 的依赖关系是显而易见的的依赖关系在构造函数中是显而易见的,但是被字段/ setter注入隐藏了。我觉得我的课就像一个“黑匣子”,带有现场/设置者注入功能。而且(根据我的经验),开发人员倾向于不打扰具有许多依赖关系的类,当您尝试使用构造函数注入来模拟类时,这是显而易见的(请参阅下一项)。困扰开发人员的问题最有可能得到解决。同样,具有过多依赖关系的类可能违反了单一职责原则(SRP)。
  3. 轻松可靠地进行模拟与现场注入相比,在单元测试中进行模拟更容易,因为您不需要任何上下文模拟或反射技术就可以到达类内部声明的私有Bean,并且不会被它愚弄。您只需实例化该类并传递模拟的bean。简单易懂。
  4. 代码质量工具可以为您提供帮助:Sonar之类的工具可以警告您该类过于复杂,因为带有很多参数的类可能是一个过于复杂的类,需要进行一些重构。当班级使用字段/设置注入时,此工具无法识别相同的问题。
  5. 更好,更独立的代码:由于代码@AutoWired之间的分布较少,因此代码对框架的依赖性较小。我知道,简单地更改项目中的DI框架的可能性并不能证明这一点(谁这样做是对的?)。但这对我来说显示了更好的代码(我通过EJB和查找很难学到了这一点)。使用Spring,您甚至可以@AutoWired使用构造函数注入来删除注释。
  6. 更多注释并不总是正确的答案出于某些原因,我确实尝试仅在注释确实有用时才使用它们。
  7. 您可以使注入不可变。不变性是一个非常受欢迎的功能。

如果您正在寻找更多信息,我建议您参阅Spring Blog的这篇较旧(但仍然相关)的文章,告诉我们为什么他们使用这么多的setter注入,并建议使用构造函数注入:

setter注入的使用频率比您预期的要多,这是因为一般而言,像Spring这样的框架更适合通过setter注入进行配置,而不是通过构造函数注入进行配置

这是关于为什么他们认为构造函数更适合应用程序代码的说明:

我认为构造函数注入对于应用程序代码比对框架代码更有用。在应用程序代码中,本质上您对配置的可选值的需求较少

最后的想法

如果您不太确定构造函数的优势,那么可以选择将setter(而非字段)与构造函数注入混合的想法,如Spring团队所解释:

由于您可以混合使用基于构造函数和基于Setter的DI,因此,将构造函数参数用于强制性依赖项并将Setters用于可选的依赖性是一个很好的经验法则

但是请记住,场注入可能是三者中应避免的一种。


-5

在课程的整个生命周期中,您的依赖性可能会改变吗?如果是这样,请使用字段/属性注入。否则,使用构造函数注入。


2
这实际上并不能解释任何事情。如果可以正确地使用私有字段,为什么还要注入私有字段呢?
罗伯特·哈维

关于私有领域,它在哪里说呢?我在说一堂课的公共界面/
Darren Young

问题中没有关于方法注入与构造函数注入的争论。JMockit的作者认为两者都不好。看问题的标题。
罗伯特·哈维

问题不是说字段注入与构造函数注入吗?
达伦·杨

1
野外注射是完全不同的动物。您正在将价值观纳入私人成员。
罗伯特·哈维
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.