首页 > 代码库 > Java命名:可怕的DefaultAbstractHelperImpl

Java命名:可怕的DefaultAbstractHelperImpl

文章来自importNew

原文链接: jaxenter 翻译: ImportNew.com - 孟冰川
译文链接: http://www.importnew.com/14241.html

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)。来自维基百科的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:

1
2
3
4
5
public interface ExecuteListener {
    default void start(ExecuteContext ctx) {}
    default void renderStart(ExecuteContext ctx) {}
    // [...]
}

*Utils 或者 *Helper

好的,这个是为mock、testing、coverage专家和狂热爱好者准备的。有个“垃圾站”来放置所有种类的静态工具方法当然是可以的。我的意思是,你当然可以成为面向对象“警察”中的一员。但是

请不要成为那样的家伙。

那 么,有各种给工具类命名的技术。理想情况下,你使用一个命名约定,然后一直使用它。例如 *Utils。我们的观点是,理想情况下你甚至可以把所有的没有和某个域有严格关联的工具方法放到一个类里面。坦白说,你上次不得不在上百万的类中去找你需要的工具方法是什么时候?从不。我们有org.jooq.impl.Utils。为什么?因为我们可以这样做:

1
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

Java命名:可怕的DefaultAbstractHelperImpl