我只是在GitHub上查看Java Hamcrest代码,并注意到他们采用的策略似乎不直观且笨拙,但它让我想知道我是否遗漏了某些东西。
我注意到在HamCrest API中有一个接口 Matcher 和一个抽象类 BaseMatcher 的。 Matcher接口使用此javadoc:
声明此方法 /**
* This method simply acts a friendly reminder not to implement Matcher directly and
* instead extend BaseMatcher. It's easy to ignore JavaDoc, but a bit harder to ignore
* compile errors .
*
* @see Matcher for reasons why.
* @see BaseMatcher
* @deprecated to make
*/
@Deprecated
void _dont_implement_Matcher___instead_extend_BaseMatcher_();
然后在BaseMatcher中,此方法实现如下:
/**
* @see Matcher#_dont_implement_Matcher___instead_extend_BaseMatcher_()
*/
@Override
@Deprecated
public final void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
// See Matcher interface for an explanation of this method.
}
不可否认,这既有效又可爱(令人难以置信的尴尬)。但是,如果每个实现Matcher的类都是为了扩展BaseMatcher,那么为什么要使用一个接口呢?为什么不首先让Matcher成为一个抽象类,让所有其他匹配器扩展它?以Hamcrest的方式做这件事有什么好处吗?或者这是不良做法的一个很好的例子?
修改
一些好的答案,但为了寻找更多细节,我正在提供赏金。我认为向后/二进制兼容性的问题是最好的答案。但是,我希望看到更多的兼容性问题,理想情况下是一些代码示例(最好是Java)。另外,“向后”兼容性和“二进制”兼容性之间是否有细微差别?
进一步编辑
2014年1月7日 - pigroxalot提供了以下答案,链接到 HamCrest的作者 this comment on Reddit 。我鼓励每个人阅读它,如果你发现它提供了信息,请upvote pigroxalot的回答。
进一步编辑
2017年12月12日 - pigroxalot的回答以某种方式被删除,不确定是怎么回事。这太糟糕了......简单的链接非常有用。
答案 0 :(得分:10)
git log
此条目自2006年12月起(初次签到后约9个月):
添加了所有Matchers应扩展的抽象BaseMatcher类。随着Matcher界面的发展,这允许未来的API兼容性[原文如此]。
我没有试图找出细节。但是,随着系统的发展,保持兼容性和连续性是一个难题。这确实意味着,如果你从头开始设计整个产品,有时你会得到一个永远不会创造出来的设计。
答案 1 :(得分:5)
但是如果意图是每个实现Matcher的类也扩展BaseMatcher,为什么要使用接口?
这不是意图。从OOP的角度来看,抽象基类和接口提供了完全不同的“契约”。
界面是通讯合约。一个类实现接口,向全世界表明它遵守某些通信标准,并根据具体参数的特定呼叫提供特定类型的结果。
抽象基类是实施合同。抽象基类由类继承,以提供基类所需的功能,但留给实现者提供。
在这种情况下,两者都重叠,但这只是一个方便的问题 - 接口是你需要实现的,而抽象类是为了使接口更容易实现 - 没有任何要求使用该基础这个类能够提供界面,只是为了减少它的工作量。您绝不限于为自己的目的扩展基类,不关心接口契约,或实现实现相同接口的自定义类。
在老式的COM / OLE代码和其他促进进程间通信(IPC)的框架中,给定的实践实际上相当普遍,在这里,它实现了与接口分离的基础 - 这正是这里所做的。 / p>
答案 2 :(得分:3)
我认为最初发生的事情是Matcher API是以界面的形式创建的 然后,在以各种方式实现接口时,发现了一个公共代码库,然后将其重构到BaseMatcher类中。
所以我的猜测是保留了Matcher接口,因为它是初始API的一部分,然后添加了描述性方法作为提醒。
通过搜索代码后,我发现接口很容易被废弃,因为它只能由BaseMatcher实现,并且可以在2个测试单元中轻松更改为使用BaseMatcher。
所以回答你的问题 - 在这种特殊情况下,除了不打破其他人的Matcher实现之外,这样做没有任何好处。
关于不良做法?在我看来,它是清晰有效的 - 所以我不这么认为,只是有点奇怪: - )
答案 3 :(得分:3)
Hamcrest仅提供匹配和匹配。这是一个小小的利基市场,但他们似乎做得很好。这个Matcher接口的实现遍布在几个单元测试库中,例如Mockito的ArgumentMatcher以及单元测试中的大量微小的匿名复制粘贴实现。
他们希望能够使用新方法扩展Matcher,而不会破坏所有现有的实现类。他们将升级到地狱。想象一下,突然让你所有的单元测试类显示出愤怒的红色编译错误。愤怒和烦恼会在一瞬间杀死hamcrest的利基市场。请参阅http://code.google.com/p/hamcrest/issues/detail?id=83了解一下。此外,hamcrest的一个重大变化会将使用Hamcrest的所有版本的库划分为更改之前和之后,并使它们互相排斥。再次,一个地狱般的场景。因此,为了保持一定的自由,他们需要Matcher成为一个抽象的基类。
但他们也在模拟业务中,接口比基类更容易模拟。当Mockito人员单位测试Mockito时,他们应该能够模仿匹配器。因此,他们还需要该抽象基类来拥有Matcher接口。
我认为他们认真考虑了这些选择,并发现这是最不好的选择。
答案 4 :(得分:2)
有一个有趣的讨论here。引用nat_pryce:
您好。我写了Hamcrest的原始版本,尽管Joe Walnes 将这个奇怪的方法添加到基类中。
原因是由于Java语言的特殊性。作为一个 下面的评论者说,将Matcher定义为基类会成功 更容易扩展库而不会破坏客户端。添加方法 到一个接口停止客户端代码中的任何实现类 编译,但可以将新的具体方法添加到抽象基础 不破坏子类的类。
但是,Java的功能只适用于接口 特别是java.lang.reflect.Proxy。
因此,我们定义了Matcher接口,以便人们可以编写 Matcher的动态实现。我们提供了基类 人们在他们自己的代码中扩展,以便他们的代码不会破坏 因为我们在界面中添加了更多方法。
我们已经将describeMismatch方法添加到Matcher中 接口和客户端代码继承了默认实现而没有 断。我们还提供了额外的基类,使其更容易 实现describeMismatch而不重复逻辑。
所以,这是一个为什么你不能盲目跟随一些通用的例子 设计方面的“最佳实践”。你必须要了解 您正在使用的工具,并在其中进行工程权衡 上下文。
编辑:将接口与基类分开也有助于应对 脆弱的基类问题:
如果将方法添加到由抽象实现的接口 基类,你可能会在基数中得到重复的逻辑 类或在子类中更改它们以实现新的 方法。您无法更改基类以删除重复的基类 逻辑如果这样做会改变提供给子类的API,因为那样 将打破所有子类 - 如果接口和。不是一个大问题 实现都在相同的代码库中,但如果你是一个坏消息 图书馆作者。
如果接口与抽象基类分开 - 也就是说, 如果你区分类型的用户和实施者的 type - 当您向接口添加方法时,您可以添加默认值 实现到不会破坏现有的基类 子类并引入一个提供更好的新基类 新子类的部分实现。有人来的时候 更改现有的子类以更好的方式实现该方法, 然后可以选择使用新的基类来减少重复的逻辑 如果这样做是有意义的。
如果接口和基类是相同的类型(有些类型) 在这个线程中建议),然后你想引入多个 以这种方式基类,你被卡住了。你不能介绍一个新的 supertype充当接口,因为那会破坏客户端 码。您无法将部分实现移动到该类型 层次结构到一个新的抽象基类,因为那将打破 现有的子类。
这同样适用于Java风格的接口和类或特性 C ++多重继承。
答案 5 :(得分:1)
Java8现在允许将新方法添加到接口(如果它们包含默认实现。
)interface Match<T>
default void newMethod(){ impl... }
这是一个很棒的工具,它让我们在界面设计和演变方面有很多自由。
但是,如果你真的想添加一个没有默认实现的抽象方法呢?
我认为您应该继续添加方法。它会打破一些现有的代码;他们将不得不修复。没什么大不了的。它可能胜过保持二进制兼容性的其他变通方法,但代价是搞砸了整个设计。
答案 6 :(得分:1)
但是,如果意图是每个实现Matcher的类 还扩展了BaseMatcher,为什么要使用界面呢?为什么不呢 首先让Matcher成为一个抽象类,并拥有其他所有类 匹配器扩展它?
通过分离接口和实现(抽象类仍然是一种实现),您遵守Dependency Inversion Principle。不要与依赖注入混淆,没有任何共同点。你可能会注意到,在Hamcrest接口中保存在hamcrest-api包中,而抽象类则在hamcrest-core中。这提供了低耦合,因为实现仅依赖于接口而不依赖于其他实现。关于这个主题的好书是:Interface Oriented Design: With Patterns。
以Hamcrest的方式做到这一点有什么好处吗?要么 这是不良做法的一个很好的例子吗?
这个例子中的解决方案看起来很难看。我认为评论就足够了。制作这种存根方法是多余的。我不会遵循这种方法。