我在Hamcrest库上做了几篇文章 ,我确实很喜欢使用它,但是我希望对其进行一些更改。 我了解他们做出的大多数设计决策,但我认为其中一些确实不值得。
介绍Litecrest
我对库所做的大多数更改都有助于减轻Hamcrest的负担,因为我觉得有些事情不必要地减轻了负担。 这就是为什么我称我的更改为Litecrest。 它不会是一个实际的库; 这只是大声思考。 我也希望您能从中学到一些有关设计库的知识。
没有说明
Description
接口以及StringDescription
和BaseDescription
类实际上并不值得。 他们提供了列表转换为字符串好看一些不错的方法,但toString()
在所有这些方法应该是足够的。 如果不是这样,可以将一些protected final
方法放在BaseMatcher
,以方便地为列表构建字符串。 当然,这并没有真正遵循SRP密切,所以你可以使用类似 Description
,以提供方便的方法。
说明,否则不是很有帮助。 它的存在性假设它专门用于提供从长远来看可能不是String的输出。 作为一个使用良好的库,将其从String更改为与输出无关的类型会破坏向后兼容,但是这种更改不太可能需要。 应用YAGNI , Description
类就在马桶下面。
无输出参数
所述describeTo()
和describeMismatch
不宜服用在Description
或附加物体的任何其它类型的字符串,尤其是作为out参数(某物,以避免尽可能经常)。 由于这些方法没有返回类型开头,因此绝对没有理由使用out参数。
仔细研究问题,您将发现根本没有理由使用参数。 我了解到,他们可能一直在试图迫使匹配器的创建者不使用String串联,但事实并非如此。 如果匹配器的描述只是一个简单的小字符串,则没有理由他们不应该仅仅返回该字符串。 就个人而言,我将删除Description
参数,并为它们提供String或CharSequence
的返回类型。 我考虑使用CharSequence
因为那样会给使用StringBuilder
带来更大的动力,但是简单地返回String也不是什么大问题,因为他们可以在其上调用toString()
。 不过,我也可能会使用CharSequence
,因为我将在断言逻辑中使用StringBuilder
来将输出放在一起,并且StringBuilder
也可以采用CharSequence
,因此唯一的toString()
必须在完成输出时被调用。
类型安全
Matcher接口采用通用参数,该参数与matches()
方法一起使用,但是所述方法采用Object
而不是通用类型。 javadoc声称这是由于类型擦除引起的,但我不认为这是一个问题。 我没有做任何挖掘来尝试是否可以将其切换为通用类型,但是如果我发现您实际上可以使用通用类型,则可以。 这消除了对TypeSafeMatcher
的需求,因为它也检查null,因此可以用更简单的NullCheckingMatcher
代替,或者只是实现它,以便断言在捕获到NullPointerException
将不匹配描述更改为“为null”。 通过执行所有这些操作,我们可能会消除所有其他双倍的基类,这些基类必须加倍以覆盖类型安全匹配器和不那么重要的匹配器。 (例如: CustomMatcher
和CustomTypeSafeMatcher
, DiagnosingMatcher
和TypeSafeDiagnosingMatcher
,以及我加倍的ChainableMatcher
,摆脱了两个DiagnosingMatcher
的影响;它们的设计很差,两次调用matches()
)
更改一些名字
我真的不喜欢describeTo()
这个名字。 应该是describeExpected()
或describeMatch()
。 我了解他们遵循JMock Constraints
的SelfDescribing
命名约定,但是看到他们没有费心完成其余方法签名的复制,实际上并没有任何好处。
CustomMatcher
S的关系被称为OneOffMatcher
S或QuickMatcher
秒。 Custom是一个令人误解的名称,听起来您需要对其进行扩展才能创建自己的匹配器。
文档中的更多示例
我不确定该库中有几个类,因为它们的文档没有显示它们的使用方式,因此我不确定它们的用处。 Condition
就是其中之一。 从少量的文档看来,这似乎是相对有用的,但是由于它没有提供使用示例(并且它是一个具有内部接口和两个内部类的相对复杂的文件),我不知道如何使用它。 它还没有记录其公共方法,因此我不确定它们是否需要大量研究。
FeatureMatcher
已得到很好的记录,但同样没有示例。
那些为图书馆编写文档的人在任何时候都牢记这一点。 如果不是很明显(通常,即使不是很明显),则应给出使用中的类的示例。
删除无关的类
其中一些已经被直接或间接地解决了。 删除Description
及其所有子类。 删除SelfDescribing
,因为它仅在Description
仍然存在时才真正有用。 删除所有TypeSafe
版本的基本匹配器。 卸下Diagnosing
匹配器。 我不确定是否应该删除Condition
因为我没有太大用。 如果保留Condition
,那么最终在核心org.hamcrest
包中有五个原始的十一个类,在api org.hamcrest
包中有四个原始的两个接口。
现在,让我们深入研究org.hamcrest.internal
包。 ArrayIterator
没什么用,因为您只能使用已经可以与foreach循环一起使用的数组。 NullSafety
似乎模仿Arrays.toList()
功能,但用IsNull
匹配器替换了null
匹配器。 我看不到这有什么帮助,因此将其删除。 ReflectiveTypeFinder
可能最终会有用。 我只看到它在TypeSafeMatcher
和FeatureMatcher
,尽管我不确定在FeatureMatcher
使用了多少。 不过,我会保留。 最后两个处理的是SelfDescribing
,我们已将其删除,因此这两个处理也是一样。 这仅使ReflectiveTypeFinder
脱离了以前的五个类。
我不打算讨论所有其他匹配器。 在大多数情况下,已添加它们的用处。 由于删除了这么多的基类,几乎所有的类都可能必须进行更改。
Lambdas!
如果将新的功能范例也应用于hamcrest,则可以扩展匹配器概念的实用性。 我没有想太多,但是对于一次性匹配器,您可以修改库以包括一个新的assertThat()
方法,如下所示:
public static void assertThat(T item, String description, Predicate matcher) {if(!matcher.test(item)) {StringBuilder output = new StringBuilder();output.append("Expected: ").append(description).append("\n but: was").append(item.toString());throw new AssertionError(output.toString());}
}
这将使您可以编写类似于以下内容的断言:
assertThat("cats", "doesn't contain \"dogs\"", str -> !str.contains("dogs"));
实际上,我实际上已经在ez-testing迷你库中添加了LambdaAssert类,因此您可以将其与原始hamcrest库一起使用。
匹配器接口
实际上有一个Matcher
接口是毫无意义的,因为hamcrest希望您扩展BaseMatcher
而不是实现Matcher
。 如果您非常不想让任何人实现,那么为什么要创建一个接口? 尤其是因为BaseMatcher
为我们做的唯一事情就是为describeMismatch()
创建一个默认实现describeMismatch()
并“实现”放置在此处的不赞成使用的方法,告诉您使用BaseMatcher
而不是Matcher
)。
如果您真的不希望人们使用该界面,请摆脱它。 就个人而言,由于无论如何我还是经常重写describeMismatch()
,所以我认为只需要实现接口就完全可以,而不必让JVM加载实际上为我提供任何东西的基类。
另外,由于我们现在有了Java 8,因此该接口可以仅使用默认方法来实现默认实现。 但是,我可以理解要避免这种情况,因为较旧的Java版本将无法利用这一点。
所以,要么只是做BaseMatcher
或没事的Matcher
正在实施。
奥托罗
我还想更改其他一些小事情,例如,迫使人们重写describeMismatch()
而不是提供默认值,但是我甚至不确定那个,因为默认值通常足够有效。 无论如何,即使您有一个受欢迎的图书馆,也并不意味着它是完美的。 始终注意进行重构。
不幸的是,所有这些更改都不是向后兼容的,但有时是值得的。
翻译自: https://www.javacodegeeks.com/2015/01/redesigning-hamcrest.html