Java命名:可怕的DefaultAbstractHelperImpl

ImportNew - 孟冰川發表於2014-12-29

JOOQ的盧卡斯·艾德 研究了在Spring和Java命名策略中富有創造性的類名所帶來的價值。

這篇文章最早是發表在jooq.org上,作為聚焦於jOOQ上所有關於Java、SQL以及軟體開發的系列的一部分。

前段時間,我們釋出了這款被我們稱作Spring API Bingo的趣味遊戲。這是對Spring構造類名時展現出的創造性極盡讚美。類名如下:

  • FactoryAdvisorAdapterHandlerLoader
  • ContainerPreTranslatorInfoDisposable
  • BeanFactoryDestinationResolver
  • LocalPersistenceManagerFactoryBean

其中有兩個是實際存在的,你能指出來嗎?如果不能,來玩下Spring API Bingo吧!

很明顯,Spring API也不得不面對下列問題……

命名

“電腦科學中只有兩個困難的問題。快取失效,命名,以及差一錯誤” –Tim Bray quoting Phil Karlton

在Java程式碼中有幾對字首和字尾非常難以擺脫。考慮到最近在推特上的討論,字首和字尾問題不可避免的引發了非常有趣的爭論。

使用介面:PaymentServce的實現:PaymentServiceImpl ,它的測試應該命名為PaymentServiceImplTest而不是PaymentServiceTest。

——Tom Bujok (@tombujok) 2014年10月8日

是的,Impl字尾是一個有趣的主題。我們為什麼要使用它,以及我們為什麼要繼續這樣命名?

說明 vs 主體

Java是一個古怪的語言。Java面世的時候,物件導向是最熱門的話題。但是過程式語言也有一些有趣的特性。當時一個非常有趣的語言叫做Ada(以及大部分源於Ada的 PL/SQL)。Ada(像PL/SQL)在包裡合理的組織了一些過程和方法,由此產生了兩種型別:說明(specification)和 主體(body)。來自維基百科的例子

-- Specification
package Example is
  procedure Print_and_Increment (j: in out Number);
end Example;

-- Body
package body Example is

  procedure Print_and_Increment (j: in out Number) is
  begin
    -- [...]
  end Print_and_Increment;

begin
  -- [...]
end Example;

你必須要這麼做,並且這兩個都要命名為Example。這兩部分要存到兩個不同的檔案中,一個叫做 Example.ads(ad出自於Ada,s出自於specification),另一個叫做Example.adb(b來自body)。PL/SQL模仿這一規範,給package檔案命名為Example.pks和Example.pkb,pk源自Package。Java走了一條不同的路,主要是因為多型和類的執行方式:

  • 類既是說明也是主體,介面不能和實現類用同樣的名字(當然主要是因為有多個實現類)。
  • 特別的,類可以分為僅說明,有部分主體的(當它們是抽象類時)以及說明和主體都有的(具體類)這幾類。

如何將這些轉化為Java中的命名

不是所有人都喜歡這種把說明和主體清楚地分開,這個顯然會有爭論。但是當你在Ada式的思維中,你很可能會希望一個介面給所有類使用,至少也應該給API暴露出來的類使用。我們在JOOQ也是這樣做的,在JOOQ,我們制定瞭如下策略來命名:

*Impl

所有的和介面(interface)有一對一關係的實現類(主體)都以Impl作為字尾。如果可以的話,儘量把這些實現放在包裡,這樣就可以封閉在包org.jooq.impl裡了。例如:

  • Cursor介面和它對應的實現CursorImpl。
  • DAO介面和它對應的實現DAOImpl。
  • Record和它的對應實現RecordImpl。

這種嚴格的命名模式使得哪個是介面,哪個是實現變得直接且清晰。我們希望Java在這方面可以更像Ada,但是我們有更好的多型,以及……

Abstract*

……使得在基類中對程式碼進行重用。 正如我們知道的,公共的基類應該(幾乎全部)總是抽象的。因為它們大部分情況下都沒有完全實現和它們對應的說明。因此, 我們有很多和介面一一對應的實現類,我們給這些部分實現類加上字首Abastract。多數情況下,這些部分實現的類也是在包內,然後封裝在org.jooq.impl包裡。

例如:

  • Field以及它的對應抽象類AbstractField。
  • Query以及它的對應抽象類AbstractQuery。
  • ResultQuery以及它的對應抽象類AbstractResultQuery。

特別的,ResultQuery是一個繼承Query的介面,thusAbstractResultQuery是一個繼承了theAbstractQuery並實現了部分方法的抽象類,而theAbstractQuery也是一個實現了部分方法的抽象類。擁有部分實現讓我們的 API更加合理,因為我們的API是一個內部的DSL(Domain-Specific Language領域特定語言)。因此,不管Fieldreally的實現是什麼,都有成千上萬的相同方法,例如Substring。

Default*

我們使用介面來做任何與API相關的事情。這在Java SE API中已經證明這是很有效的,例如:

  • Collections
  • Streams
  • JDBC
  • DOM

我們也使用介面來做任何與SPI(服務提供介面)相關的事情。API和SPI之間有一個很重要的區別,按照API的演化:API由使用者來使用而不實現,SPI由使用者實現但不使用。如果你不開發JDK(因此不用理會那些令人發瘋的向後相容規則),你基本上可以很安全的在API介面中新增新方法。

事 實上,我們在比較小的版本釋出時會這樣做。因為我們沒有期望任何人去實現我們的DSL(誰會想要實現Field的286個方法,或者DSL的677個方 法,簡直是瘋了)。但是SPI不同。無論什麼時候只要向你的使用者提供了SPI,你就不能簡單地增加新方法——至少在Java8之前不能增加,因為那樣會破壞實現類,並且這樣的實現類有很多。

然後,我們仍然這樣做,因為我們沒有像JDK那樣的向後相容的規則。我們的規則更加輕鬆。但是我們仍不建議使用者直接自己實現介面,但是可以繼承一個空的Defaultimplementation。例如ExecuteListener以及它的對應空實現DefaultExecuteListener:

public interface ExecuteListener {
    void start(ExecuteContext ctx);
    void renderStart(ExecuteContext ctx);
    // [...]
}

public class DefaultExecuteListener
implements ExecuteListener {

    @Override
    public void start(ExecuteContext ctx) {}

    @Override
    public void renderStart(ExecuteContext ctx) {}

    // [...]
}

所以,通常Default*是一個用來為API使用者提供的,可以使用並且例項化的公共實現類的字首。或者用作SPI實現可以繼承的字首(不用冒著向後相容的風險)。這差不多是Java 6、Java 7′缺乏介面預設方法的臨時解決辦法,這也是為什麼命名字首更加合適的原因。

這條規則的Java 8的版本

事實上,這個實踐證明定義Java 8相容SPI一個好用的規則是使用介面並且使所有方法都有一個預設的空方法體。如果JOOQ不支援Java 6,我們很可能像下面這個定義我們的ExecuteListener:

public interface ExecuteListener {
    default void start(ExecuteContext ctx) {}
    default void renderStart(ExecuteContext ctx) {}
    // [...]
}

*Utils 或者 *Helper

好的,這個是為mock、testing、coverage專家和狂熱愛好者準備的。有個“垃圾站”來放置所有種類的靜態工具方法當然是可以的。我的意思是,你當然可以成為物件導向“警察”中的一員。但是

請不要成為那樣的傢伙。

那 麼,有各種給工具類命名的技術。理想情況下,你使用一個命名約定,然後一直使用它。例如 *Utils。我們的觀點是,理想情況下你甚至可以把所有的沒有和某個域有嚴格關聯的工具方法放到一個類裡面。坦白說,你上次不得不在上百萬的類中去找你需要的工具方法是什麼時候?從不。我們有org.jooq.impl.Utils。為什麼?因為我們可以這樣做:

import static org.jooq.impl.Utils.*;

你幾乎擁有貫穿整個程式的“頂層方法”一樣的東西。我們覺得“全域性”方法也是很好的東西。我們是絕不買“我們沒辦法mock”之類討論的賬,所以不要試圖挑起爭論。

討論

或者,事實上,讓我們掀起一場討論。你的技巧是什麼,為什麼?下面是對Tom BujokTweet原文的回應,來幫助你開始這場爭論:

@tombujok 不。PaymentServiceImplTestImpl! — Konrad Malawski (@ktosopl) October 8, 2014

@tombujok 放棄使用介面

— Simon Martinelli (@simas_ch) October 8, 2014

@tombujok 給所有的東西填上Impl字尾。

— Bartosz Majsak (@majson) October 8, 2014

@tombujok @lukaseder @ktosopl 根原因是類不應該被叫做 *Impl,但是我知道你們一直故意挑起我們的爭論。 — Peter Kofler (@codecopkofler) October 9, 2014

相關文章