簡體   English   中英

為什么堅持接口的所有實現都擴展了基類?

[英]Why insist all implementations of an interface extend a base class?

我只是看着GitHub上的Java Hamcrest代碼,並注意到他們采用了一種似乎不直觀和笨拙的策略,但它讓我想知道我是否遺漏了一些東西。

and an abstract class . 我在HamCrest API中注意到有一個接口和一個抽象類 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的作者對Reddit評論 我鼓勵每個人閱讀它,如果你發現它提供了信息,請upvote pigroxalot的回答。

即便進一步編輯

2017年12月12日 - pigroxalot的答案以某種方式被刪除,不知道是怎么回事。 這太糟糕了......簡單的鏈接非常有用。

git log有此條目,從2006年12月(初始簽入后約9個月):

添加了所有Matchers應該擴展的抽象BaseMatcher類。 隨着Matcher接口的發展,這允許未來的API兼容性[原文如此]。

我沒有試圖弄清楚細節。 但是,隨着系統的發展,保持兼容性和連續性是一個難題。 它確實意味着,如果你從頭開始設計整個產品,有時你最終會得到一個永遠不會創造的設計。

但是,如果每個實現Matcher的類都是為了擴展BaseMatcher,那么為什么要使用一個接口呢?

這不完全是意圖。 抽象基類和接口從OOP角度提供完全不同的“契約”。

接口通信合同。 接口由類實現 ,以向世界表明它遵守某些通信標准,並且將響應具有特定參數的特定呼叫給出特定類型的結果。

抽象基類實現合同。 抽象基類由類繼承 ,以提供基類所需的功能,但留給實現者提供。

在這種情況下,兩者都重疊,但這只是一個方便的問題 - 接口是你需要實現的,並且抽象類是為了使接口更容易實現 - 沒有任何要求使用該基類是能夠提供界面,只是為了減少工作量。 您絕不限於為自己的目的擴展基類,不關心接口契約,或實現實現相同接口的自定義類。

在老式的COM / OLE代碼和其他框架中,給定的實踐實際上相當普遍,這些框架促進了進程間通信(IPC),它使得實現與接口分離成為基礎 - 這正是這里所做的。

我認為最初發生的事情是,最初是以界面的形式創建了Matcher API。
然后,在以各種方式實現接口時,發現了一個公共代碼庫,然后將其重構到BaseMatcher類中。

所以我的猜測是保留了Matcher接口,因為它是初始API的一部分,然后添加了描述性方法作為提醒。

在搜索完代碼之后,我發現界面可以很容易地完成,因為它只能由BaseMatcher實現,並且可以在2個測試單元中輕松更改為使用BaseMatcher。

所以回答你的問題 - 在這種特殊情況下,除了不打破其他人的Matcher實現之外,這樣做沒有任何好處。

至於不好的做法? 在我看來,這是明確和有效的 - 所以不,我不這么認為,只是有點奇怪:-)

Hamcrest僅提供匹配和匹配。 這是一個小小的利基市場,但他們似乎做得很好。 這個Matcher接口的實現遍布了幾個單元測試庫,例如Mockito的ArgumentMatcher以及單元測試中的大量微小的匿名復制粘貼實現。

他們希望能夠使用新方法擴展Matcher,而不會破壞所有現有的實現類。 他們將升級到地獄。 想象一下,突然讓你所有的單元測試類顯示出憤怒的紅色編譯錯誤。 憤怒和煩惱會在一瞬間殺死hamcrest的利基市場。 請參閱http://code.google.com/p/hamcrest/issues/detail?id=83,了解其中的一小部分內容。 此外,hamcrest的一個重大變化會將使用Hamcrest的所有版本的庫划分為更改之前和之后,並使它們互相排斥。 再次,一個地獄般的場景。 因此,為了保持一定的自由,他們需要Matcher成為一個抽象的基類。

但它們也在模擬業務中,接口比基類更容易模擬。 當Mockito人員單位測試Mockito時,他們應該能夠模仿匹配器。 因此,他們還需要該抽象基類來擁有Matcher接口。

我認為他們認真考慮了這些選擇,發現這是最不好的選擇。

有一個關於它的有趣的討論在這里 引用nat_pryce:

你好。 我編寫了Hamcrest的原始版本,盡管Joe Walnes將這種奇怪的方法添加到基類中。

原因是由於Java語言的特殊性。 作為下面的評論者,將Matcher定義為基類可以更容易地擴展庫而不會破壞客戶端。 向接口添加方法會阻止客戶端代碼中的任何實現類進行編譯,但可以在不破壞子類的情況下將新的具體方法添加到抽象基類中。

但是,Java的功能只適用於接口,特別是java.lang.reflect.Proxy。

因此,我們定義了Matcher接口,以便人們可以編寫Matcher的動態實現。 我們為人們提供了基類,以便他們在自己的代碼中進行擴展,以便在我們向接口添加更多方法時,他們的代碼不會中斷。

之后我們將describeMismatch方法添加到Matcher接口,客戶端代碼繼承了默認實現而沒有破壞。 我們還提供了額外的基類,使得在不重復邏輯的情況下更容易實現describeMismatch。

因此,這就是為什么在設計時不能盲目遵循一些通用的“最佳實踐”的原因。 您必須了解您正在使用的工具,並在該環境中進行工程權衡。

編輯:將接口與基類分離也有助於解決脆弱的基類問題:

如果將方法添加到由抽象基類實現的接口,則在更改它們以實現新方法時,最終可能會在基類或子類中出現重復邏輯。 如果這樣做會改變提供給子類的API,則無法更改基類來刪除重復的邏輯,因為這會破壞所有子類 - 如果接口和實現都在相同的代碼庫中,那么這不是一個大問題,但如果你的話,那就是壞消息圖書館作者。

如果接口與抽象基類是分開的 - 也就是說,如果區分類型的用戶和類型的實現者 - 當您向接口添加方法時,可以向基類添加默認實現,而不是打破現有的子類並引入一個新的基類,為新的子類提供更好的部分實現。 當有人來更改現有的子類以更好的方式實現該方法時,如果有意義的話,可以選擇使用新的基類來減少重復的邏輯。

如果接口和基類是相同的類型(正如一些人在這個線程中建議的那樣),然后你想以這種方式引入多個基類,那么你就會陷入困境。 您不能引入新的超類型作為接口,因為這將破壞客戶端代碼。 您不能將部分實現沿着類型層次結構移動到新的抽象基類中,因為這會破壞現有的子類。

這同樣適用於Java樣式接口和類或C ++多重繼承的特性。

Java8現在允許將新方法添加到接口(如果它們包含默認實現)。

interface Match<T>

    default void newMethod(){ impl... }

這是一個很棒的工具,它為我們提供了很多界面設計和演變的自由。

但是,如果您真的想添加一個沒有默認實現的抽象方法呢?

我想你應該繼續添加方法。 它會打破一些現有的代碼; 他們將不得不修復。 沒什么大不了的。 它可能勝過保持二進制兼容性的其他變通方法,但代價是搞砸了整個設計。

但是,如果每個實現Matcher的類都是為了擴展BaseMatcher,那么為什么要使用一個接口呢? 為什么不首先讓Matcher成為一個抽象類,讓所有其他匹配器擴展它?

通過分離接口和實現(抽象類仍然是一個實現),您遵守依賴性倒置原則 不要與依賴注入混淆,沒有任何共同點。 你可能會注意到,在Hamcrest接口中保存在hamcrest-api包中,而抽象類則在hamcrest-core中。 這提供了低耦合,因為實現僅依賴於接口而不依賴於其他實現。 關於這個主題的好書是: 面向接口的設計:使用模式

以Hamcrest的方式做這件事有什么好處嗎? 或者這是不良做法的一個很好的例子?

這個例子中的解決方案看起來很難看。 我認為評論就足夠了。 制作這種存根方法是多余的。 我不會遵循這種方法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM