我已经读到,在构造函数中使用“ new”(对于除简单值对象之外的任何其他对象)是一种不好的做法,因为它使单元测试变得不可能(因为这些协作者也需要创建并且不能被模拟)。由于我没有真正的单元测试经验,因此我试图收集一些我将首先学习的规则。另外,无论使用哪种语言,这是一条通常有效的规则吗?
new
在不同的语言中意味着不同的事物。
new
关键字。您在问什么语言?
我已经读到,在构造函数中使用“ new”(对于除简单值对象之外的任何其他对象)是一种不好的做法,因为它使单元测试变得不可能(因为这些协作者也需要创建并且不能被模拟)。由于我没有真正的单元测试经验,因此我试图收集一些我将首先学习的规则。另外,无论使用哪种语言,这是一条通常有效的规则吗?
new
在不同的语言中意味着不同的事物。
new
关键字。您在问什么语言?
Answers:
总是有例外,我对标题中的“ always”始终表示怀疑,但是,是的,该准则通常是有效的,并且也适用于构造函数之外。
在构造函数中使用new违反了SOLID中的D(依赖关系反转原理)。因为单元测试全都与隔离有关,所以它使您的代码难以测试。如果类具有具体的引用,则很难隔离它。
不过,这不仅仅涉及单元测试。如果我想一次将存储库指向两个不同的数据库怎么办?在我自己的上下文中传递的能力使我可以实例化指向不同位置的两个不同的存储库。
在构造函数中不使用new可以使您的代码更加灵活。这也适用于可能使用构造而不是new
对象初始化的语言。
但是,显然,您需要使用良好的判断力。在很多情况下,可以使用new
它,或者最好不要使用它,但不会造成负面影响。在某个地方,new
必须调用它。请注意new
在许多其他类都依赖的类中进行调用时要非常小心。
进行诸如在构造函数中初始化一个空的私有集合之类的事情很好,而将其注入将是荒谬的。
new
从其内部调用它。head = null
任何方式带来的所有特殊情况来改善代码?为什么将包含的集合保留为null并按需创建它们比在构造函数中这样做更好?
虽然我赞成使用构造函数来仅初始化新实例而不是创建多个其他对象,但可以使用辅助对象,并且您必须根据自己的判断来判断某个对象是否是内部辅助对象。
如果该类表示一个集合,则它可能具有内部帮助器数组或列表或哈希集。它会new
用来创建这些帮助器,并且被认为是很正常的。该课程不提供使用其他内部帮手的注射,也没有理由。在这种情况下,您要测试对象的公共方法,这些方法可能用于累积,删除和替换集合中的元素。
从某种意义上说,编程语言的类构造是用于创建更高级别抽象的机制,并且我们创建此类抽象以弥合问题域和编程语言原语之间的鸿沟。但是,类机制只是一种工具;它随编程语言的不同而变化,并且在某些语言中,某些域抽象仅需要编程语言级别的多个对象。
总而言之,您必须做出一些判断:抽象是否只需要一个或多个内部/帮助对象,同时仍被调用方视为单个抽象,或者其他对象是否可以更好地暴露给调用方以创建对象控制依赖关系,例如,在调用者在使用类时看到这些其他对象时,将建议使用此控件。
并非所有协作者都足够有趣,可以单独进行单元测试,您可以(通过托管/实例化类)(间接)对其进行测试。这可能与某些人需要测试每个类,每个公共方法等的想法不一致,尤其是在之后进行测试时。使用TDD时,您可以重构此“合作者”,从测试优先流程中提取出一个已经完全处于测试状态的类。
Not all collaborators are interesting enough to unit-test separately
故事的结尾:-),这种情况是可能的,没有人敢提。@Joppe我鼓励您详细说明答案。例如,您可以添加一些类的示例,这些类仅是实现细节(不适合替换),以及在我们认为有必要的情况下如何提取它们。
new File()
该文件以执行与文件相关的任何事情,那么禁止该调用是没有意义的。您将要做什么,针对stdlib的File
模块编写回归测试?不见得。另一方面,召唤new
一个自学成才的班级则更加可疑。
new File()
。
由于我没有真正的单元测试经验,因此我试图收集一些我将首先学习的规则。
认真学习“规则”以解决从未遇到的问题。如果你遇到一些“规则”或“最佳实践”,我会建议寻找的治所在今是“应该”被使用,并试图解决这个问题的一个简单的玩具如自己,忽略了什么“规定”说。
在这种情况下,您可以尝试提出2或3个简单的类以及它们应实现的某些行为。以自然的方式实现类,并针对每种行为编写单元测试。列出您遇到的任何问题,例如,如果您以某种方式开始工作,然后不得不回头并在以后进行更改;如果您对事物应该如何融合感到困惑;如果您不喜欢写样板;等等
然后尝试按照“规则”解决相同的问题。同样,列出您遇到的问题。比较列表,并考虑遵循规则时哪种情况可能会更好,哪些情况可能不会更好。
至于您的实际问题,我倾向于使用端口和适配器方法,在此方法中我们将“核心逻辑”和“服务”区分开来(这类似于区分纯函数和有效过程)。
核心逻辑就是根据问题域计算应用程序内部的事物。它可能包含类,如User
,Document
,Order
,Invoice
,等它的优良具有核心类调用new
其他核心类,因为他们是“内部”的实施细则。例如,创建Order
可能还会创建Invoice
和Document
详细说明已订购的商品。无需在测试期间模拟这些内容,因为这些是我们要测试的实际内容!
端口和适配器是核心逻辑与外界交互的方式。这就是事情喜欢Database
,ConfigFile
,EmailSender
,等活。这些都是使测试变得困难的事情,因此建议在核心逻辑之外创建它们,并根据需要将它们传递(通过依赖项注入或作为方法参数等)。
这样,就可以自己测试核心逻辑(这是特定于应用程序的部分,重要的业务逻辑所处的位置,并受到最大的影响),而不必关心数据库,文件,电子邮件等。我们可以只传递一些示例值,然后检查是否获得正确的输出值。
可以使用数据库,文件系统等的模拟对端口和适配器进行单独测试,而不必关心业务逻辑。我们可以只传递一些示例值,并确保它们已被存储/读取/发送/等。适当地。
请允许我回答这个问题,在这里收集我认为是关键点的内容。为了简洁起见,我将引用一些用户。
总是有例外,但是是的,该规则通常是有效的,并且也适用于构造函数之外。
在构造函数中使用新型干法d在SOLID(依赖倒置本金)。因为单元测试全都与隔离有关,所以它使您的代码难以测试。如果类具有具体的引用,则很难隔离它。
-TheCatWhisperer-
是的,使用new
内部构造函数通常会导致设计缺陷(例如紧密耦合),这会使我们的设计僵化。很难测试,但并非不可能。这里发挥的特性是弹性(对变化的容忍度)1。
但是,以上引用并不总是正确的。在某些情况下,可能会有一些类被认为是紧密耦合的。David Arno评论了几对。
当然,在类是不可变值 对象,实现细节等的情况下,存在例外。应该将它们紧密耦合。
-大卫·阿尔诺-
究竟。一些类(例如,内部类)可能仅仅是主类的实现细节。这些旨在与主类一起进行测试,并且不一定可替换或可扩展。
此外,如果我们的SOLID崇拜使我们提取这些类,则可能违反了另一个好的原则。所谓的得墨meter耳定律。另一方面,从设计的角度来看,这真的很重要。
因此,像往常一样,可能的答案取决于。使用new
内部构造函数可能不是一个好习惯。但并非总是系统地。
因此,需要我们评估这些类是否是主类的实现细节(大多数情况下不是)。如果是的话,别管它们。如果不是,请考虑使用诸如IoC Containers的成分根或依赖性注入之类的技术。
1:SOLID的主要目标不是使我们的代码更具可测试性。这是为了使我们的代码更能容忍更改。更加灵活,因此更易于测试
注意: TheWhisperCat的David Arno,希望您不要介意我引用了您。
是的,在应用程序根类中使用“ 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
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.
为什么这是个问题?并非依赖树中的每个依赖都应该可以替换
new
关键字不是一个坏习惯,而且从来没有。它是如何使用该工具的事项。例如,您无需使用大锤即可满足要求的大锤。