第6章 面向接口而非实现进行编码
第六章 面向接口而非实现进行编码
-------------------------------------------------------
一直以来,都有一个编程规则,那就是面向接口编程,实现跟接口两部分,在编写代码时,让系统的其他部分只依赖于接口,规则早于java语言就存在,但这个技巧中蕴含的道理可谓无价。
接下来我们来讲解,是使用面向接口还是面向实现来公开API
(1)移除方法或者字段
如果一个接口或者类已经对外发布了,那么是否还能从中删除一个方法和字段呢?事实上一旦有人使用了,就不能移除。这种问题,就是源代码上的不兼容。而且这种改变在二进制层面也是无法兼容,如果将这些字段或者属性设计成为private和package的访问权限,那么就不会有移除之后编译错误的问题,但是如果有些使用者运用了反射,但是这种情况就是属于滥用反射,已经超出了设计者的预期,也和良好的API使用习惯相违背,这真是破坏规则者容易出现问题的根源所在。对于protected的访问权限,因为只能在同一个包下访问,也会有一些子类来继承它,除非将protected设计成final类型,但是这种设计完全没有必要,所以我们在设计api中,尽量不要这么去做。
(2)移除或者添加一个类或者接口
对于一个公开的API接口或者实现,我们一般都不会将她进行移除,它就应该一直存在下去,所以在设计的时候,一定要考虑这种类和接口是否有存在下去的意义。
(3)向现有的继承体系中添加一个类或者接口
在特定的环境下,向现有的继承体系中添加一个类或者接口可以带来很多好处,比如下面的代码,存在两个名的sayHello的抽象方法。
public abstract String sayHello(); public abstract String sayHello(String who);
在接下来的版本中,作者希望可以简化这个API,所以提供了另外一个类,可以支持第一个无参方法的调用,代码如下:
public abstract class SimpleHelloClass { public abstract String sayHello(); }
当然,以前编写的类仍然需要维护,所以讲以前的类改成继承SimpleHellloClass这个新类,同时可以添加更加复杂的方法,,对于这种改变在二进制和源代码的层面上都是兼容的,也是向后兼容的。唯一的限制就是所有的方法(不管是继承还是自定义的方法),只要来自原始接口或者SimpleHelloClass这个类,都要保持一致,至少不能移除某个方法。
(4)添加字段和方法
添加字段前面已经提及,对于API的字段最好是通过方法去访问,与方法相比,字段受到的约束更大,这正是要避免在API中使用字段的区别,当然对于static和final类型的字段还是可以添加到API中的,添加此类字段是可以的,同时也能满足二进制和源代码兼容和向后兼容。
避免在允许继承的类或者接口中添加新的抽象方法,如果是类,请确保该类是不能被继承。
还有一种小技巧,如果向一个API的类中添加新的方法,那么通过使用新的方法和新类型,就可以保证兼容性的问题,也不会引发潜在的问题。因为新的类型是不复存在的,但是如果有调用方继承了这个类,但是也添加了一样的类型,那么也会有问题,但是这种问题概率还是比较小。
(5)Java中接口和类的区别
Java最突出的特性就是继承,接口和多实现,而类只能单继承。
对性能的追求是唯一实现的理由,因为多继承可以减少对内存的占用,利用多继承使用一个对象,就可以实现API中公开的多个接口,类继承只能由一个父类,此时如果类之间有些数据需要互访或者调用,那么只能用委托的方法,这样的继承对于内存的占用是比较大的,可以看一下具体的数据,假设基于英特尔的CPU,在大部分32位的Java虚拟机上,一个对象实例,不管他实现多少接口,他只会占有8个字节的内存,但如果是一个引用了其他类型的类,那么每一个对象实例至少要16字节,相比之下,一大堆这样的对象占用很多内存,这样继承自接口,就会好过继承类,但这种明显的内存占用,只有创建了非常多的对象时才会出现,如果是一两个,就不是那么明显了。但是一般情况下,性能方面的考虑不是主要因素了。
(6)弱点背后的优点
从接口的演变角度来看,一个接口中添加字段或者方法,如果要强调向后兼容的话,那这个是几乎不能完成的任务,但这样的一个弱点,反而成为了接口的一大优点。
通常来说,如果提供API,也会相应的版本,比如说对于JAva而言,就会有jdk1.3,jdk1.4....,这些版本的特性也不相同,对于一个接口而言,提供了一个良好的机会,可以分别定义language12,language13.。。等等这些接口,并分别实现这三个接口,用户在使用时可以根据自己的实际情况来选择相应的实现,这样就可以使用相应的功能,而且代码看起来也更加清楚,在编写方法和编写代码的过程中,对于语言的不同版本,要决定分别支持哪些语言,就这点而言,接口是一个非常适合的工具。
虽然看起来很美,这里仍然隐藏了一个陷阱,那就是,接口膨胀会极大的提高客户端代码的复杂度。
(7)添加方法的另一种方案
如果需要给一个类中添加新方法,就选择final类进行添加;在final类中添加新方法不会像接口一样将客户端的代码高的很复杂;例如下面的final类型的代码:
import java.util.concurrent.Callable; public final class InstanceProvider { private final Callable<Object> instance; public InstanceProvider(Callable<Object> instance) { this.instance = instance; } public Class<?> instanceClass() throws Exception { return instance.call().getClass(); } public Object instanceCreate() throws Exception { return instance.call(); } public static void main(String[] args) throws Exception { InstanceProvider provider = new InstanceProvider(new Callable<Object>() { @Override public Object call() throws Exception { return new BaseClass(); } }); System.out.println(provider.instanceClass().getName()); } }
现在就很容易的在这个类中添加新方法,并提供默认实现,调整后代码如下:
import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; public final class InstanceProvider { private final Callable<Object> instance; private final Set<String> types; /** * 不仅需要一个工厂来创建对象实例,还需要一些关于创建对象实例时的额外信息 * @param instance 用来创建对象实例时的工厂 * @param types 用来标识要创建的对象实例所述的类型 */ public InstanceProvider(Callable<Object> instance, String... types) { this.instance = instance; this.types = new HashSet<String>(); this.types.addAll(Arrays.asList(types)); } public Class<?> instanceClass() throws Exception { return instance.call().getClass(); } public Object instanceCreate() throws Exception { return instance.call(); } /** * 用来确认给定的instanceprovider是否可以创建指定类型的对象实例 * 这种检查无需创建真正的对象或者将相应的实现类加载到内存中 * @param c 要验证的类 * @return 判断当前给定的instance provider是否可以创建上一个参数指定的类。 * @throws Exception */ public boolean isInstanceOf(Class<?> c) throws Exception { if (types != null) { return types.contains(c.getName()); } else { return c.isAssignableFrom(instanceClass()); } } public static void main(String[] args) throws Exception { InstanceProvider provider = new InstanceProvider(new Callable<Object>() { @Override public Object call() throws Exception { return new BaseClass(); } }); System.out.println(provider.instanceClass().getName()); } }
对于客户端来说,完全不用关心,所以维护起来比较方便。
(8)抽象类有没有用呢
相对于java接口,使用java抽象类还有一些其他方面的优势,那就是抽象类可以包含static的方法,即使想把所有的内容都使用抽象方法来描述,但也可能需要有些static工厂方法作为基本的入口,那么就可能需要抽象类,开发人员可以通过Javadoc得知相应的static方法,作为整个API的入口,不过也可以使用一个接口加上一个独立的工厂类,后者专门来提供static工厂方法,具体的选择安全取决于个人的喜好。
在Java中,抽象类可以设计限制访问权限,接口则做不到。
(9)要为增加参数做好准备
这种方式可以通过InstanceProvider方式解决,但是比较好的解决方式就是request/response模式。
如果要采取这种模式,一定要把Request和Response类都申明为final类,如果将他们声明为接口,在对这个系统进行改进时,就会平添很多麻烦。
(10)接口VS.类
总而言之,编程时应该面向接口而非面向实现,请牢记一点,所谓的接口并不是指Java中的interface,而是指抽象内容,如果使用Java进行编程,就使用interface来标识不可变类型,同时使用final类作为可以添加方法的类型,在设计更复杂的结构时,需要考虑扩展性,在深入思考后选择合适的方式,如前面给说的Request/Response模式,有时候,使用抽象类也是可接受的,只不过,要记住如果希望达到百分之百的二进制兼容,绝不要向接口或者可子类化的类中添加方法。