1. 抽象类和接口的区别
所谓抽象类是用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象;所谓接口,相当于电源插座,可插入构件相当于电器。可插入构件的关键在于存在一个公用的接口,以及每个构件都实现了这个接口。接口是实现构件的可插入性的关键。
1.1. 从语法层区别抽象类和接口
从语法层来讲,Java语言对于抽象类和接口给出了不同的定义方式,下面以定义一个名为Demo的抽象类为例来说明这种不同。
Demo抽象类的定义方式如下:
abstract class Demo {
abstract void method1();
abstract void method2();
…
}
Demo的接口定义方式如下:
interface Demo {
void method1();
void method2();
…
}
在抽象类的定义中,Demo可以有自己的数据成员,也可以有非abstract的成员方法,而在接口的定义中,Demo只能够有static final数据成员,所有的成员方法都是abstract的。从某种意义上说,接口是一种特殊形式的抽象类。
从编程的角度来看,首先,抽象类和接口的继承规则不同,抽象只允许单继承,而一个类却可以实现多个接口。接口对于多重继承的支持方面的一种折中考虑;其次,在抽象类的定义中,可以赋予方法的默认行为,而在接口的定义中,方法不能拥有默认行为,必须使用委托,从某种意义上来说,接口比抽象类更为抽象化。
1.2. 从设计层上理解抽象类和接口
上面主要从语法层的角度论述了抽象类和接口的区别,这些层面的区别是比较低层次的、非本质的。本小节将从这个设计层进行分析理解二者概念的本质。
抽象类在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"Is-A"关系,即父类和子类在概念本质上应该是相同的。对于接口来说则不然,接口并不要求实现者和接口定义在概念本质上是一致的,仅仅是实现了接口定义的契约而已。考虑这样一个例子,假设在有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过抽象类或者接口来定义一个表示该抽象概念的类型,定义方式分别如下所示:
Door抽象类的定义方式如下:
abstract class Door {
abstract void open();
abstract void close();
}
Door的接口定义方式如下:
interface Door {
void open();
void close();
}
其他具体的Door类型可以extends使用抽象类方式定义的Door或者implements使用接口方式定义的Door。看起来好像使用抽象类和接口没有大的区别。如果现在要求Door还要具有报警的功能。下面将罗列出可能的解决方案,并从设计层对方案进行分析。
解决方案一:
简单的在Door的定义中增加一个alarm方法,如下:
abstract class Door {
abstract void open();
abstract void close();
abstract void alarm();
}
或者
interface Door {
void open();
void close();
void alarm();
}
那么具有报警功能的AlarmDoor的定义方式如下:
class AlarmDoor extends Door {
void open(){…}
void close(){…}
void alarm(){…}
}
或者
class AlarmDoor implements Door{
void open(){…}
void close(){…}
void alarm(){…}
}
这种方法违反了接口隔离原则,在Door的定义中把Door概念本身固有的行为方法和另外一个概念“报警器”的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为“报警器”这个概念的改变(比如:修改alarm方法的参数)而改变,反之依然。
解决方案二:
既然open、close和alarm属于两个不同的概念,根据接口隔离原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个概念都使用抽象类方式定义;两个概念都使用接口方式定义;一个概念使用抽象类方式定义,另一个概念使用接口方式定义。
显然,由于Java语言不支持多重继承,所以两个概念都使用抽象类方式定义是不可行的。后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。
如果两个概念都使用接口方式来定义,那么就反映出两个问题:第一,我们可能没有理解清楚问题领域,AlarmDoor在概念本质上 到底是Door还是报警器?第二,如果我们对于问题领域的理解没有问题,比如:我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用接口方式定义)反映不出上述含义。
如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,抽象类在Java语言中表示一种继承关系,而继承关系在本质上是"Is-A"关系。所以对于Door这个概念,我们应该使用抽象类方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过接口方式定义。如下所示:
abstract class Door{
abstract void open();
abstract void close();
}
interface Alarm{
void alarm();
}
class AlarmDoor extends Door implements Alarm{
void open(){…}
void close(){…}
void alarm(){…}
}
这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实抽象类表示的是"Is-A"关系,interface表示的是"Has-A"关系,在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有 Door的功能,那么上述的定义方式就要反过来了。
2. 抽象机制的重要性
抽象机制是面向对象的可复用设计的必要条件。
面向对象的可复用设计的基石是开闭原则,也即一个软件实体应该对扩展开放,对修改关闭。满足开闭原则的关键就在于抽象化。在Java这样的面向对象的编程语言里,可以给系统定义出一个一劳永逸,不在更改的抽象设计,此设计允许有无穷无尽的行为在实现层被实现。在Java语言里,可以给出一个或者多个抽象类或接口,规定出所有的可能的扩展,因此在任何扩展的情况下都不会改变。这就使得系统的抽象层不需修改,从而满足了开闭原则的第二条:对修改关闭。同时,由于从抽象层导出一个或者多个新的具体类可以改变系统的行为,因此系统对扩展是开放的,这就满足了开闭原则的第一条:对扩展开放。
所有的软件系统都有一个共同的性质,即对它们的需求都会随时间的推移而发生变化,在软件系统面临新的需求时,系统的设计必须是稳定的。满足开闭原则的设计可以给软件系统带来一定适应性和灵活性,使变化中的软件系统具有一定的稳定性和延续性。
3. 结论
抽象类和接口是Java语言中的两种定义抽象类的方式,它们之间有很大的相似性。但是对于它们的选择却又往往反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理,因为它们表现了概念间的不同的关系。只有正确理解面向对象的设计原则并灵活使用抽象类和接口,才能设计出易用的系统