在构造函数中使用“ new”总是不好吗?


37

我已经读到,在构造函数中使用“ new”(对于除简单值对象之外的任何其他对象)是一种不好的做法,因为它使单元测试变得不可能(因为这些协作者也需要创建并且不能被模拟)。由于我没有真正的单元测试经验,因此我试图收集一些我将首先学习的规则。另外,无论使用哪种语言,这是一条通常有效的规则吗?


9
它不会使测试变得不可能。但是,它的确会使维护和测试代码变得更加困难。以读新的胶为例。
大卫·阿尔诺

38
“总是”总是不正确的。:)有最佳实践,但也有很多例外。
保罗,

63
这是什么语言?new在不同的语言中意味着不同的事物。
user2357112'2

13
这个问题取决于语言,不是吗?并非所有语言都具有new关键字。您在问什么语言?
布莱恩·奥克利

7
愚蠢的统治。当您与“ new”关键字关系不大时,将无法使用依赖项注入。与其说“如果使用new来打破依赖关系倒置是一个问题”,不如说“不要破坏依赖关系倒置”。
Matt Timmermans '18

Answers:


36

总是有例外,我对标题中的“ always”始终表示怀疑,但是,是的,该准则通常是有效的,并且也适用于构造函数之外。

在构造函数中使用new违反了SOLID中的D(依赖关系反转原理)。因为单元测试全都与隔离有关,所以它使您的代码难以测试。如果类具有具体的引用,则很难隔离它。

不过,这不仅仅涉及单元测试。如果我想一次将存储库指向两个不同的数据库怎么办?在我自己的上下文中传递的能力使我可以实例化指向不同位置的两个不同的存储库。

在构造函数中不使用new可以使您的代码更加灵活。这也适用于可能使用构造而不是new对象初始化的语言。

但是,显然,您需要使用良好的判断力。在很多情况下,可以使用new它,或者最好不要使用它,但不会造成负面影响。在某个地方,new必须调用它。请注意new在许多其他类都依赖的类中进行调用时要非常小心。

进行诸如在构造函数中初始化一个空的私有集合之类的事情很好,而将其注入将是荒谬的。

一个类对它的引用越多,就应该越小心,不要new从其内部调用它。


12
我真的没有看到这条规则的任何价值。关于如何避免代码之间的紧密耦合,存在一些规则和准则。该规则似乎处理了完全相同的问题,除了它的限制过于严格之外,并没有给我们带来任何其他价值。那有什么意义呢?为什么要为我的LinkedList实现创建一个虚拟标头对象,而不是处理以head = null任何方式带来的所有特殊情况来改善代码?为什么将包含的集合保留为null并按需创建它们比在构造函数中这样做更好?
Voo

22
您错过了重点。是的,持久性模块绝对是高级模块不应该依赖的。但是,“从不使用新的东西” 并非源于“应该注入某些东西,而不应将它们直接耦合”。如果您获得了敏捷软件开发的可信赖副本,您会发现Martin通常在谈论模块而不是单个类:“高级模块处理应用程序的高级策略。这些策略通常并不关心实现的细节。他们。”
Voo

11
因此,关键是要避免模块之间的依赖性,尤其是不同抽象级别的模块之间的依赖性。但是一个模块可以不止一个类,并且在同一抽象层的紧密耦合代码之间根本没有点建立接口。在遵循SOLID原则的精心设计的应用程序中,并非每个单独的类都应实现一个接口,并且并非每个构造函数都应始终仅将接口作为参数。
Voo

18
我之所以投票,是因为它有很多流行语,而且没有太多关于实际考虑的讨论。关于将两个数据库回购到一起的事情,但是老实说,这不是真实的例子。
jpmc26 '18

5
@ jpmc26,BJoBnh无需考虑是否同意这个答案,这一点非常清楚。如果您认为“依赖关系反转主体”,“具体引用”或“对象初始化”只是流行语,那么实际上您只是不知道它们的含义。那不是答案的错。
R. Schmitz '18

50

虽然我赞成使用构造函数来仅初始化新实例而不是创建多个其他对象,但可以使用辅助对象,并且您必须根据自己的判断来判断某个对象是否是内部辅助对象。

如果该类表示一个集合,则它可能具有内部帮助器数组或列表或哈希集。它会new用来创建这些帮助器,并且被认为是很正常的。该课程不提供使用其他内部帮手的注射,也没有理由。在这种情况下,您要测试对象的公共方法,这些方法可能用于累积,删除和替换集合中的元素。


从某种意义上说,编程语言的类构造是用于创建更高级别抽象的机制,并且我们创建此类抽象以弥合问题域和编程语言原语之间的鸿沟。但是,类机制只是一种工具;它随编程语言的不同而变化,并且在某些语言中,某些域抽象仅需要编程语言级别的多个对象。

总而言之,您必须做出一些判断:抽象是否只需要一个或多个内部/帮助对象,同时仍被调用方视为单个抽象,或者其他对象是否可以更好地暴露给调用方以创建对象控制依赖关系,例如,在调用者在使用类时看到这些其他对象时,将建议使用此控件。


4
+1。这是完全正确的。您必须确定类的预期行为,并确定哪些公开细节需要公开,哪些不需要公开。在确定班级的“责任”时,您还需要玩一种精妙的直觉游戏。
jpmc26

27

并非所有协作者都足够有趣,可以单独进行单元测试,您可以(通过托管/实例化类)(间接)对其进行测试。这可能与某些人需要测试每个类,每个公共方法等的想法不一致,尤其是在之后进行测试时。使用TDD时,您可以重构此“合作者”,从测试优先流程中提取出一个已经完全处于测试状态的类。


14
Not all collaborators are interesting enough to unit-test separately故事的结尾:-),这种情况是可能的,没有人敢提。@Joppe我鼓励您详细说明答案。例如,您可以添加一些类的示例,这些类仅是实现细节(不适合替换),以及在我们认为有必要的情况下如何提取它们。
Laiv

@Laiv域模型通常是具体的,非抽象的,因此您无需在其中注入嵌套对象。不具有逻辑的简单值对象/复杂对象也是候选对象。
若佩

4
绝对是+1。如果设置了一种语言,则您必须调用new File()该文件以执行与文件相关的任何事情,那么禁止该调用是没有意义的。您将要做什么,针对stdlib的File模块编写回归测试?不见得。另一方面,召唤new一个自学成才的班级则更加可疑。
Kilian Foth,

7
@KilianFoth:幸运的是,单元测试可以直接调用任何东西new File()
Phoshi

1
那是个猜测。我们可以找到无意义的情况,无用的情况以及无用的情况。这是需求和偏好的问题。
Laiv

13

由于我没有真正的单元测试经验,因此我试图收集一些我将首先学习的规则。

认真学习“规则”以解决从未遇到的问题。如果你遇到一些“规则”或“最佳实践”,我会建议寻找的治所在今是“应该”被使用,并试图解决这个问题的一个简单的玩具如自己,忽略了什么“规定”说。

在这种情况下,您可以尝试提出2或3个简单的类以及它们应实现的某些行为。以自然的方式实现类,并针对每种行为编写单元测试。列出您遇到的任何问题,例如,如果您以某种方式开始工作,然后不得不回头并在以后进行更改;如果您对事物应该如何融合感到困惑;如果您不喜欢写样板;等等

然后尝试按照“规则”解决相同的问题。同样,列出您遇到的问题。比较列表,并考虑遵循规则时哪种情况可能会更好,哪些情况可能不会更好。


至于您的实际问题,我倾向于使用端口和适配器方法,在此方法中我们将“核心逻辑”和“服务”区分开来(这类似于区分纯函数和有效过程)。

核心逻辑就是根据问题域计算应用程序内部的事物。它可能包含类,如UserDocumentOrderInvoice,等它的优良具有核心类调用new其他核心类,因为他们是“内部”的实施细则。例如,创建Order可能还会创建InvoiceDocument详细说明已订购的商品。无需在测试期间模拟这些内容,因为这些是我们要测试的实际内容!

端口和适配器是核心逻辑与外界交互的方式。这就是事情喜欢DatabaseConfigFileEmailSender,等活。这些都是使测试变得困难的事情,因此建议在核心逻辑之外创建它们,并根据需要将它们传递(通过依赖项注入或作为方法参数等)。

这样,就可以自己测试核心逻辑(这是特定于应用程序的部分,重要的业务逻辑所处的位置,并受到最大的影响),而不必关心数据库,文件,电子邮件等。我们可以只传递一些示例值,然后检查是否获得正确的输出值。

可以使用数据库,文件系统等的模拟对端口和适配器进行单独测试,而不必关心业务逻辑。我们可以只传递一些示例值,并确保它们已被存储/读取/发送/等。适当地。


6

请允许我回答这个问题,在这里收集我认为是关键点的内容。为了简洁起见,我将引用一些用户。

总是有例外,但是是的,该规则通常是有效的,并且也适用于构造函数之外。

在构造函数中使用新型干法dSOLID(依赖倒置本金)。因为单元测试全都与隔离有关,所以它使您的代码难以测试。如果类具有具体的引用,则很难隔离它。

-TheCatWhisperer-

是的,使用new内部构造函数通常会导致设计缺陷(例如紧密耦合),这会使我们的设计僵化。很难测试,但并非不可能。这里发挥的特性是弹性(对变化的容忍度)1

但是,以上引用并不总是正确的。在某些情况下,可能会有一些类被认为是紧密耦合的。David Arno评论了几对。

当然,在类是不可变值 对象,实现细节等的情况下,存在例外。应该将它们紧密耦合

-大卫·阿尔诺-

究竟。一些(例如,内部类)可能仅仅是主类的实现细节。这些旨在与主类一起进行测试,并且不一定可替换或可扩展。

此外,如果我们的SOLID崇拜使我们提取这些,则可能违反了另一个好的原则。所谓的得墨meter耳定律。另一方面,从设计的角度来看,这真的很重要。

因此,像往常一样,可能的答案取决于使用new内部构造函数可能不是一个好习惯。但并非总是系统地。

因此,需要我们评估这些类是否是主类的实现细节(大多数情况下不是)。如果是的话,别管它们。如果不是,请考虑使用诸如IoC Containers的成分根依赖性注入之类的技术。


1:SOLID的主要目标不是使我们的代码更具可测试性。这是为了使我们的代码更能容忍更改。更加灵活,因此更易于测试

注意: TheWhisperCat的David Arno,希望您不要介意我引用了您。


3

作为一个简单的示例,请考虑以下伪代码

class Foo {
  private:
     class Bar {}
     class Baz inherits Bar {}
     Bar myBar
  public:
     Foo(bool useBaz) { if (useBaz) myBar = new Baz else myBar = new Bar; }
}

因为new是的纯实现细节Foo,并且Foo::BarFoo::Baz都是的一部分Foo,所以在进行单元测试时Foo,没有必要模拟的部分Foo。您只能在进行单元测试时在外面 模拟零件。FooFoo


-3

是的,在应用程序根类中使用“ new”是一种代码味道。这意味着您正在将该类锁定为使用特定的实现,并且将无法替换其他实现。始终选择将依赖项注入到构造函数中。这样,您不仅可以在测试过程中轻松注入模拟的依赖关系,还可以通过允许您在需要时快速替换不同的实现方式来使您的应用程序更加灵活。

编辑:对于拒绝投票的人-这是指向软件开发书的链接,其中将“新”标记为可能的代码味道:https ://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 #v = onepage&q = new%20keyword%20code%20smell&f = false


15
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.为什么这是个问题?并非依赖树中的每个依赖都应该可以替换
Laiv

4
@Paul:拥有默认实现意味着您可以紧密引用指定为默认的具体类。不过,这并没有使其成为所谓的“代码气味”。
罗伯特·哈维

8
@EzoelaVacca:在任何情况下,我都会谨慎使用“代码气味”一词。这有点像在说“共和党人”或“民主人士”。这些话甚至是什么意思?给您这样的标签后,您就不必再思考真正的问题了,学习也就停止了。
罗伯特·哈维

4
“更灵活”不会自动“更好”。
whatsisname

4
@EzoelaVacca:使用new关键字不是一个坏习惯,而且从来没有。它是如何使用该工具的事项。例如,您无需使用大锤即可满足要求的大锤。
罗伯特·哈维'18
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.