为什么坚持接口的所有实现都扩展了基类?

时间:2013-12-20 22:16:07

标签: java inheritance interface abstract-class hamcrest

我只是在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的回答以某种方式被删除,不确定是怎么回事。这太糟糕了......简单的链接非常有用。

7 个答案:

答案 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的方式做到这一点有什么好处吗?要么   这是不良做法的一个很好的例子吗?

这个例子中的解决方案看起来很难看。我认为评论就足够了。制作这种存根方法是多余的。我不会遵循这种方法。