马克思曾说,运动是物质固有的根本属性,是一切物质形态的存在方式。万事万物无不在运动之中。一个好的程序不但需要经受bug的考验,还得经受时间的冲刷。那些不能够随时代而更迭的程序,无一不被时代淘汰,成为历史。
既然时代在交替,而程序在设计结束之时就完成了,那我们怎么让程序做到与时俱进呢?有一条不得不说的原则:“开放-关闭原则”(注:意为对扩展开放,对修改关闭)。要让程序能够符合这一原则,面向接口编程这一概念就呼之欲出了。
接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
如同每台电脑有USB接口、充电器接口等等不同接口。同一个接口,接入的设备不同则对应的功能不同。JAVA的接口也是如此,如果我们开放了接口,程序的可拓展新就能够得到满足。举个例子:同样的“读”、“写”功能,或许U盘和MP3Player的不同,但只要它们继承了接口来重写方法,则最终在电脑里调用时都是调用read()、write()。
下面用一个例子来具体说明:
假设:现在我们要开发一个应用,模拟不同类型货车的装货与卸货。
上下文(环境):已知要实现轻型卡车(下面简称轻卡)、重型卡车(下面简称重卡)、火车三种运货设备,要求工厂能同这三种货车进行物资的装载或者卸载,并且以后可能会有新的运货机器出现,所以工厂能够装货的车型必须有扩展性,能与目前未知而以后可能会出现的货车进行物资交换。各种货车间上货、下货的实现方法不同,轻卡和重卡只有这两个方法,火车还有一个车厢分离(假设为Separating())方法。
名词定义:数据交换={装,卸}
解决方案列举
方案一:分别定义LightTruck、HeavyTruck、Train三个类,实现各自的Load()和UnLoad()方法。然后在Factory类中实例化上述三个类,为每个类分别写装,卸方法。例如,为LightTruck写Load、UnLoad两个方法。总共六个方法。
方案二:定义抽象类Trunk,在里面写虚方法Load和UnLoad,三个货运设备继承此抽象类,并重写Load和UnLoad方法。Factory类中包含一个类型为Car的成员变量,并为其编写get/set器,这样Factory中只需要两个方法:Loading_cargo和Unloading_cargo,并通过多态性实现不同移动设备的读写。
方案三:与方案二基本相同,只是不定义抽象类,而是定义接口ITrunk,三个货运类实现此接口。Factory中通过依赖接口ITrunk实现多态性。
下面,我们来分析一下以上四种方案:
首先,方案一最容易想到和实现,but它有一个致命的弱点:可扩展性差。当将来时代变迁了,有了新的货运设备时,必须对Factory进行修改。这就如在一个真实的工厂里,为每一种货运设备修一块各自的停车场、并分别有各自的管理设施。当有了一种新的货运设备后,我们就要将原来的场地全部拆除重新划分,再给新的货运设备一块场地。这种设计显然不可取。
此方案的另一个缺点在于,冗余代码多。如果有100种货运设备,那我们的Factory类中岂不是要至少写200个方法? It's Unacceptable.
再看 方案二和方案三,他们基本是一个方案,只不过实现手段不同,一个是使用了抽象类,一个是使用了接口,而且最终达到的目的应该是一样的。
首先很明显的是:它解决了代码冗余的问题。因为可以动态替换货运方式,并且都实现了共同的接口,所以不管有多少种货运方式,只要一个Load方法和一个Unload方法,多态性就帮我们解决问题了。而对第一个问题,由于可以运行时动态替换,所以有了新的设备时,完全可以替换进去运行。这就是所谓的“依赖接口,而不是依赖与具体类”,不信你看看,Factory类只有一个Trunk类型或ITrunk类型的成员变量,至于这个变量具体是什么类型,它并不知道,这取决于我们在运行时给这个变量的赋值。如此一来,Factory和Trunk类的耦合度大大下降。
那么 这里该选抽象类还是接口呢?看动机。这里,我们的动机显然是实现多态性而不是为了代码复用,所以当然要用接口。
所以,我们选择方案三来实现:
首先编写ITrunk接口:
public interface ITrunk {
void Load(); //装货
void Unload(); //卸货
}
然后是三种交通工具的类,都继承ITrunk接口,并重写Load()和Unload():
public class LightTruck implements ITrunk{
public void Load() {
System.out.println("Load to LightTruck");
System.out.println("Load finished!");
}
public void Unload() {
System.out.println("Unload from LightTruck");
System.out.println("Unload finished!");
}
}
public class HeavyTruck implements ITrunk{
public void Load() {
System.out.println("Load to HeavyTruck");
System.out.println("Load finished!");
}
public void Unload() {
System.out.println("Unload from HeavyTruck");
System.out.println("Unload finished!");
}
}
public class Train implements ITrunk{
public void Load() {
System.out.println("Load to Train");
System.out.println("Load finished!");
}
public void Unload() {
System.out.println("Unload from Train");
System.out.println("Unload finished!");
}
//车厢分离
public void Separating(){
System.out.println("The carriage is separating.");
}
}
下面来写Factory类:
public class Factory {
private ITrunk trunk;
public Factory() {
this.trunk = null;
}
public Factory(ITrunk trunk) {
this.trunk = trunk;
}
public ITrunk getTrunk() {
return trunk;
}
//设置用何种交通工具
public void setTrunk(ITrunk trunk) {
this.trunk = trunk;
}
//工厂类实现装货
public void Loading_cargo(){
this.trunk.Load();
}
//工厂类实现卸货
public void Unloading_cargo(){
this.trunk.Unload();
}
}
到此,所有的类就写完了。结果对不对呢?我们再写一个Test类来检验一下:
public class Test {
public static void main(String args[]){
Factory myfac = new Factory();
ITrunk mytunk1 = new LightTruck();
ITrunk mytunk2 = new HeavyTruck();
ITrunk mytunk3 = new Train();
//用三种货车来运输
System.out.println("1.use LightTruck:");
myfac.setTrunk(mytunk1);
myfac.Loading_cargo();
System.out.println("=================");
System.out.println("2.use HeavyTruck:");
myfac.setTrunk(mytunk2);
myfac.Loading_cargo();
System.out.println("=================");
System.out.println("3.use Train:");
myfac.setTrunk(mytunk3);
myfac.Loading_cargo();
System.out.println("=================");
}
}
运行结果如下:
可见,结果完全符合预期。如果按照面向接口编程的思路来写程序,则程序的多态性和可扩展性都将变得容易操作。
然而,在java中还有一种机制能实现不断扩展程序的功能,更好的实现与时俱进,名唤“反射”。反射顾名思义,就如同一面镜子,它将当前已经有的类照下来,并复制一份作为副本,这个过程我们可以得到原有类的构造器及参数,得到原有类的方法,我们可以操作这个类去实现新的功能,让它成为一个新的类。
详情请看下一篇文章,程序的与时俱进之二——JAVA的反射机制。