写在前面
文中可能很多地方出现了在程序设计中的基础问题,请各位程序设计的达人不要见笑。为了区别Java中的关键字abstract class和OO编程中的抽象类,我在本文中分别把它们叫做抽象类和抽象的类。
abstract class和interface是Java语言中对于类的抽象的定义进行支持的两种方式。可是,由于abstract class和interface之间在对于类的抽象方面具有很大的相似性,有的时候甚至可以相互替换,因此很多开发人员在进行类的抽象时,对于abstract class和interface的选择显得比较随意。其实,两者之间还是有很大的区别的。对于面向对象的(就是Object Oriented,简称OO,也译做“物件导向的”)方法的理解,对于类的派生关系的理解,都直接影响到在类的抽象过程中的定义。本文主要说明我对于这个问题的一些理解。
理解“抽象”(abstract)的类
抽象是在类的定义之前,需要做的一项复杂的工作。在具体的软件系统实现之前,我们需要对系统进行设计,其中的一项就是要明确有哪些类需要定义在系统之中,它们之间的关系如何等等。而对于一个经验不是很丰富的程序员来说,这样的设计好像又有些勉为其难了。因为我们也许根本不知道系统需要哪些类,即使知道必需哪些类,还是对它们的分析不能到达非常清楚的程度。当我们自行开发应用系统的时候,往往在设计出系统需求之后,根本就无从下手。只有当我们拿到一份详细设计书的时候,我们才知道系统要被做成什么样子的。我们似乎觉得实现才是程序员非常清楚的事情,而非设计。但是没有人能阻止程序员前进的步伐,因为程序员不甘于永远只做程序员。
首先,我们来看传统的OO编程中的抽象这一过程:当软件系统中出现了形式相似、本质相同的类的时候,我们需要把这些类的共性拿出来,放到比这些类更高一层的地方(Pull up),做为这些类的父类(super class),而将它们的不同,保留在这些类中,最后,让这些类都去继承共性的父类,以消除重复定义的代码。在现实中,也有很多相似的情形。比如:如果我们进行一个图形编辑软件的开发,就会发现在这个软件系统中存在着圆、三角形等等一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,而形状这一概念不能以一种具体形式出现在在软件系统中,那么它就是一个抽象概念。正是因为抽象的概念在系统中没有具体的Object与其对应,所以用以表征抽象概念的抽象的类是不能够实例化的。在Java中,abstract class和interfac在类的抽象的方面表现有些相似,都可以在Java技术中表现类的共性,都是对类进行抽象后得到的概念性的一种定义。
在OO的程序设计过程中,抽象的类主要用来进行类的共性的提取。我们可以构造出一个固定的一组行为的抽象定义,但是对于这一组行为,每个不同的种类又可能有自己不同的实现方式。这组抽象定义集合起来就是我们通常所说的抽象的类,而派生的非抽象的类中则定义了这一组可能的具体实现。在程序设计过程中,有一个核心的原则——OCP(Open-Closed Principle),是在代码控制中减少代码重用和清楚的划分代码性质的一个很好的原则。对于不熟悉OCP的朋友,Object Mentor(www.objectmentor.com)中给出了对OCP的描述:"Open For Extension" and "Closed for Modification",直译过来是对扩展开放,对修改封闭,但是我并不喜欢这样去理解,因为已经有一个成型的翻译,即弱藕合、强聚合。在O.M.网站上的原文解释是这样的:
Modules that conform to the open-closed principle have two primary attributes.
1. They are “Open For Extension”.
This means that the behavior of the module can be extended. That we can make the module behave in new and different ways as the requirements of the application change, or to meet the needs of new applications.
2. They are “Closed for Modification”.
The source code of such a module is inviolate. No one is allowed to make source code changes to it.
事实上,我们在做类的设计及编码的时候,有可能并不知道其它的类是怎么样的,它们有什么方法,有什么可用的属性等等;或许,在我们的类完成后又出现了一种新的情况,即有更多的类适用于当前设计和编码。而我们却知道它们的标准,它们的共性,有什么样可供使用的方法和属性。这样,我们就可以通过对这些类的标准来对新出现的或未知的对象进行操作了。
回到刚才的例子,我们要做一个图形图像软件的开发,我们需要做一个Drawer以在输出设备上输出平面图形,此时,我们知道有一些图形,如三角形,圆形等,我们定义了Trianglel、Circle...等图形,我们要做图像输出的时候,需要判断每一个点是否在形状的内部等等信息,而对于每个为类来说,它们的属性又不尽相同:我们都知道,空间中的三个点确定一个三角形,所以,三角形这个类中,如果有了三个顶点的信息,它就是一个完整的三角形了;而圆却不同,通过确定圆心坐标和半径就可以确定一个具体的圆形。而我们判断一个点是否在图形的内部而把图形的内部进行填充的时候,我们不能通过每个图形的每个属性根据不同的类形来做判断,而是要根据它们相通的性质来进行判断。这就要用到抽象的类的技术了。
从语法定义的角度来看abstract class和interface
在语法层面,Java语言对于abstract class和interface给出了不同的定义方式,下面以定义了名为DemoAbstractClass的抽象类DemoInterface接口和来说明它们之间的异同。
// DemoAbstractClass.java
abstract class DemoAbstractClass {
// fields goes here ...
DataTypeA fieldA = initializeValue;
// methods goes here ...
ReturnTypeA method1() { }
ReturnTypeB method2() { }
abstract ReturnTypeC method3();
}
// DemoInterface.java
interface DemoInterface {
// methods goes here ...
void method1();
void method2();
}
我想试图通过上述例子来表现抽象类和接口的异同。通常情况下,抽象类可以拥有自己的数据成员,可以拥有抽象的方法声明和非抽象的方法定义,其中,抽象的方法声明的时候需要在方法之前加abstract关键字。而在接口的定义中,通常只包含只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface是一种特殊形式的abstract class。
从编程的角度来看,abstract class和interface都可以用来实现"design by contract"的思想。但是在具体的使用上面还是有一些区别的。
首先,abstract class在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。
其次,在abstract class的定义中,允许有非抽象的方法。但是在interface的定义中,并不能允许有非抽象的方法,也就是说,所有的方法都需要在实现者中被具体的定义其方法体。
如果不能在抽象的类中定义默认行为,就会导致同样的方法实现出现在该抽象的类的每一个派生类中,违反了"one rule,one place"原则,造成代码重复,同样不利于以后的维护。因此,在abstract class和interface间进行选择时要非常的小心。
从设计思想的角度来看abstract class和interface
上面主要从语法定义和编程的角度论述了abstract class和interface的区别,这些层面的区别是比较低层次的、非本质的。下面我将从设计思想方面,谈谈我的认识。
前面已经提到过,abstarct class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is a"关系,也就是说,派生类是父类的一个具体形式,或者是在某一特定情况下的体现。对于interface 来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已,换一句话说,就是实现了接口的类和被实现的接口之间的关系是一个"has a ... FUNCTION"的关系。举个例子:
假设有一个关于门的抽象概念,门具有两个基本动作open和close,此时我们可以通过abstract class或者interface来定义一个表示这个抽象概念的类型:
使用abstract class方式定义DoorAbstractClass:
abstract class DoorAbstractClass {
/** @return boolean Returns if the door has been opened successful */
abstract boolean open();
/** @return boolean Returns if the door has been closed successful */
abstract boolean close();
}
使用interface方式定义DoorInterface:
interface DoorInterface {
/** @return boolean Returns if the door has been opened successful */
boolean open();
/** @return boolean Returns if the door has been closed successful */
boolean close();
}
其他具体的门的类型可以扩展DoorAbstractClass或者实现DoorInterface来完成对类的抽象。目前看来好像使用abstract class和interface没有大的区别。
如果现在要求门还要具有报警的功能。我们该如何设计类的结构呢?下面将罗列出我想到的解决方案:
解决方案一:
简单的在上述两个类的抽象的定义中增加一个alarm方法,如下:
abstract class DoorAbstractClass {
abstract boolean open();
abstract boolean close();
abstract boolean alarm();
}
interface DoorInterface {
boolean open();
boolean close();
boolean alarm();
}
那么具有报警功能的AlarmDoor的定义方式如下:
class AlarmDoor1 extends DoorAbstractClass {
boolean open() {
// method content ...
return false;
}
boolean close() {
// method content ...
return false;
}
boolean alarm() {
// method content ...
return false;
}
}
或者
class AlarmDoor2 implements DoorInterface {
boolean open() {
// method content ...
return false;
}
boolean close() {
// method content ...
return false;
}
boolean alarm() {
// method content ...
return false;
}
}
这种方法在门这个类的定义中把门这个概念本身固有的行为方法和另外一个概念“报警器”的行为方法混在了一起。这样引起了一些问题:那些仅仅依赖于报警门这个类的方法会因为“报警器”这个类的实现的不同(如:修改alarm方法的参数)而发生改变。它违反了OO设计中的另一个核心原则ISP(Interface Segregation Principle)。
解决方案二:
既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个抽象的类都使用abstract class方式定义;两个都使用interface方式定义;一个使用abstract class方式定义,另一个使用interface方式定义。
当然,只有后面两种方式是可以通过Java的编译的。但是对于该问题的理解、对于设计意图的能否合理反映,就决定了选择哪一种方式。
如果两个概念都使用interface方式来定义,那么就反映出两个现象:1、我们可能根本不清楚“具有报警功能的门”到底是门还是报警器?2、如果我们对于该门是门还是报警器有相对确切的划分,我们通过分析发现AlarmDoor本来就是一扇门和,那么我们在实现时就没有能够正确的揭示我们的设计意图,这两个接口在的定义上(均使用interface方式定义)的地位是均等的,反映不出上述含义。
通常情况下,我们的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,abstract class在Java语言中表示一种继承关系,而继承关系在本质上是"is a"关系。所以对于Door这个概念,我们应该使用abstarct class方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。如下所示:
abstract class Door {
abstract boolean open();
abstract boolean close();
}
interface Alarm {
boolean alarm();
}
class AlarmDoor3 extends Door implements Alarm {
boolean open() {
// method content ...
return false;
}
boolean close() {
// method content ...
return false;
}
boolean alarm() {
// method content ...
return false;
}
}
这种实现方式基本上能够明确的反映出我们对于这个问题的理解,较准确的揭示我们的设计意图。其实,以abstract class表示"is a"的关系、interface表示"has a ... FUNCTION"的关系作为依据,在这两者之间进行选择还是相对容易的。当然这是被我们所设计和实现的问题所处在的环境所限定的。比如:如果我们认为AlarmDoor在本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。
题外:假设我有一套组合音响,它对于我的家来说,可以说是一件陈列品象征着我的品位,也可以是一件家用电器用来放音乐。当我们考虑实现一套虚拟现实的软件的时候,我们需要对它进行分类,那么,一套组合音响到底算一件陈列品还是算一件家用电器怎么样区分呢?考虑这样一个问题的时候,需要了解拥有者的真正用意,就是它用它来做什么?有时,我们自己也很难区别一套组合音响“是一件”陈列品或者“是一件”家用电器,它“有一种”陈列品的“功能”还是“有一种”家用电器的“功能”。如果当真要在这个问题上面纠缠下去的话,那只有它的使用者才能知道它到底是什么了。
结论
abstract class和interface是Java技术中的两种对类抽象的方式,它们之间有很大的相似性。但是对于它们的选择却又往往反映出对于问题的理解,是否能正确、合理的反映设计意图,因为它们表现了类之间的不同的关系(虽然都能够实现需求的功能)。这其实也是Java技术的细枝末节,也充分的体现了Java技术的特点。