扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
在Java的世界,有一些API的设计原则是很通用的,如尽量使用接口的方式来表达所有的API(不要使用类来描述API)。接口自有它的价值,但是将所有的API都通过接口来表示并不见得总是一个好的设计方案。在使用一个接口来描述API时,必须有一个足够的理由。下面给出了一些理由:
1、接口可以被任何人所实现。假设String是一个接口而非类,永远都无法确认用户提供的String实现能够遵循你希望的规则:字符串类是一个不变量;它的hashCode是按照一定的算法规则来返回数字;而length永远都不会是一个负数等。如果真的由用户来提供一个String的实现,可以想象代码中要加入多少异常处理代码和相关的判断语句才能保证程序的健壮性。
实践告诉我们,如果API完全是由接口来定义,用户在使用这些API时会发现不得不进行大量的强制转型(译注:个人认为,强制转型并不是因为API是通过接口来定义引起的,而是不好的API定义引起的,而且强制转型从程序的设计角度几乎是无法避免的,除非所有的子类都不添加任何新的功能,而这一点与前面的抽象类演化又是矛盾的)。
2、接口不可能拥有构造函数或者是static方法。如果需要接口的实例,不可能直接实例化接口,只能通过某种方式,可能是new也可能是通过参数传递的方式来获得一个接口的具体实现对象。当然,这个对象可能是由你来实现的,也可能是由第三方供应商开发的。如果Integer是一个接口而非一个类,则无法通过new Integer(int)来构造一个Integer对象,可能会通过一个IntegerFactory.newInteger(int)来获得一个Integer对象实例,天啊,API变得更加复杂和难以理解了。
(译注:个人认为原文作者有些过于极端化了,因为Java不是一个纯粹的API,它同时是一种语言,一个平台,所以提供的String和Integer,都是作为基础类型来提供。虽然同意作者的观点,但是作者使用的上述例子,个人认为不是很有说服力。)
3、接口无法进行演化。假设在2.0版本的API中为一个接口添加一个方法,多米诺骨牌倒了,大量直接实现了这个接口的类,根本就无法通过编译,直到实现了这个方法为止。 当然可以在调用这个新方法的时候,通过捕捉AbstractMethodError这个异常来保证二进制的兼容性,但是如此笨重的方法,实在不是智者所为。除非告诉用户说千万不要直接实现这个接口,请先继承所提供的那个抽象类,这样做就不会有问题了,不过用户会尖锐的责问:那为什么要提供这样一个接口,不如直接提供一个抽象类算了。
4、接口是不可以被序列化的,虽然Java的序列化存在许多问题,但是仍然不可避免的要用到它。象JMX的API就严重依赖于序列化接口。序列化是针对序列化接口的子类来处理的,当一个可序列化的对象被反序列化时,就有会一个相同的新对象被重新创建出来。如果这个子类没有提供一个public构造函数,那么可能很难在程序中使用这个功能,因为只能反序列化而不能进行序列化。而且在反序列化时,只能使用接口进行强制转型。如果要序列化的内容是一个类,那就不需要提供这样的序列化接口。
当然,对于下列情况,接口还是非常有用的:
1、回调:如果功能完全由用户来实现,在这种情况下,接口显然比抽象类更加合适。例如Runnable接口。特别是那些通常只含有一个方法的,往往是接口,而非类(译注:最常用的就是各种Listener)。如果一个接口中包含有大量的抽象方法,用户在实现这个接口的时候,就不得不必须实现一些空方法。所以对于有多个方法的接口,建议提供一个抽象类,这样在接口中添加新的方法,而不需要强迫用户实现新的方法。(译注:看来作者很推荐使用接口+基类的方式来编写API,不过Java SE本身就是这样做的,例如MouseAdapter,KeyAdapter等。个人认为,如果是规范,当然最好是接口,象J2EE规范;如果是框架,或者是功能包,还是建议使用接口+基类的方式。所谓的回调其实是SPI(Service Provider Interface)的一种)。
2、多重继承:在一个继承体系比较深的结构里,可以通过接口来实现多重继承。Comparable是一个最好的例子,比如Integer实现了Comparable接口,因为Integer的父类是Number,所以通过接口的方式实现了多重继承。但是在Java的核心类库中,这样的经典例子并不多。通常一个类实现了多个接口并不一定是一个好的设计,因为这往往将许多责任强加在一个类上,有违基本的设计原则,而且很容易产生重复代码。如果真的需要这样一个功能,使用一个匿名类或者是一个内部类来实现这些接口,或者使用一个抽象类作为基类也是不错的方案。
3、动态代理:价值不可估量的动态代理类java.lang.reflect.Proxy class 可以在运行的时候根据接口生成实现的内容。它将对一个接口的调用转换成对某一个对象具体方法的调用,非常的灵活,可以有效的减少代码重复。但是对于一个抽象类,就不可能动态生成一个代理对象了。如果喜欢使用动态代理技术,那么使用接口对软件开发是非常有效的。(CGLIB有时可以有效地对抽象类实现动态代理,但是有许多限制,而且其文档也较少。)
谨慎的分包:
Java在控制类和方法的可见性上,所支持的方式实在乏善可陈,除了public,proteced,private以外,就只能通过pakcage来控制。如果一个类或者方法想让外部的包可见,则所有的类和方法都可以访问它了,不能指定外部哪些类可以访问自身。这就意味着如果将API分成若干个包进行发布,则必须对这些包详细设计,避免减少API的公开性。 最简单的方法当然是把所有的API放在一个包中,这样很容易通过package来降低访问性。如果API不超过30个类,这个方案简直是完美。
但事事往往不尽如人事,经常API非常大,不适合放在一个包中。这时候可能要不得不进行私有分包了(这里的私有与private不一样的,只是一种伪私有),私有只是不在JavaDoc中输出这些类的文档信息。如果查看JDK,会发现许多以sun.*或者com.sun.*打头的包相关的文档信息并没有包含在JDK的JavaDoc中。如果开发人员主要通过JavaDoc来使用API,那可能根本不会注意到这些包的存在,只有查看源代码或者分析API的人才能看到这些API内容。即使发现了这些没有通过文档公开的类,也不建议使用它们,因为不通过文档公开的API,往往也意味着它可能会随着时间的改变进行演化,也有可能在演化的过程中不能保持兼容性。(译注:象C#支持assembly的访问机制,个人就感觉很好,象Osgi支持Bundle,允许定制输出类也是不错误的解决方案,不过前者是语言级,而后者是框架级。)
还有将包隐藏起来的一个方式就是在包的名称中包含internal。所以Banana的API可能会有公开的包com.example.banana, com.example.banana.peel,也可能还有s com.example.banana.internal 和com.example.banana.internal.peel。
别忘记所谓的私包同样是可以访问的,更多时候这样的私包只是出于安全的考虑,建议用户不要随便访问,并没有任何语言级的约束。还有一些技术可以解决这个问题,例如NetBeans的API教程中就给出了一种解决方案。在JMX的API中,则使用了另外一种方式。象javax.management.JMX这个类,就只提供了static方法而没有提供public构造函数。这也就意味着你不能实例化这样一个类。(译注:不明白这个例子的意义。)
下面在设计JMX时的一些技巧
不变类是一个很好的设计,如果一个类可以设计成不变类,就不要用可变类!如果详细了解这样设计的原因,请参见《Effective Java》的第十三条。如果没有读过这本书,很难设计出好的API。
另外字段信息应该是private的,只有static和final修饰的字段信息才能变成public,允许外部访问。这一条是一个非常基础的原则,这里提到这个原则,只是因为在早期的API设计时,有些API违反了这个原则,这里不再给出一个例子了。
避免奇怪的设计。对于Java代码,已经有了许多约定俗成的方法了,如get/set方法,标准的异常类。即使觉得有了更好的方法,也尽量避免使用这些方法。如果使用了一些奇怪的方法名称,这样使用API的用户必须学习新的内容,不能按照原有的习惯来理解代码,会增加学习成本,也会增加误用的可能。
再举个例子,象java.nio以及java.lang.ProcessBuilder就是一个不好的设计,它不使用getThing()和setThing()方法这种方式,而使用了thing()和thing(T)这两个方法。许多人认为这是一个不错的设计方法,但是这样违反了常用的方法设计原则,强迫用户来学习这种API。(译注:java.nio和java.lang.ProcessBuilder是指JDK6中的包,害得我在JDK1.4中找了半天,参见http://download.java.net/jdk6/doc/api/java/lang/ProcessBuilder.html,这里所谓的thing和Thing也不是真有这个方法和类,而是ProcessBuilder中的command和command(List)等多个方法。)
不要实现Cloneable, 即使想某一个类支持对象的复制,这个接口也没有太多的价值,如果真想支持复制功能,提供一个复制构造函数或者是一个static方法来复制对象,又或者提供一个static的工厂方法来创建对象,也会更加有效。例如想让Banana这个类拥有clone的能力,可以使用代码如下:
public Banana(Banana b) { // copy constructor this(b.colour, b.length); } // ...or... public static Banana newInstance(Banana b) { return new Banana(b.colour, b.length); } |
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者