在系统设计时,常常遇到这样一个问题:类Client 的实例 instanceClient 希望使用另一个对象instanceX 提供的服务service,但在设计时,我们并不能确定对象instanceX究竟属于哪个类。遇到这种情况的时候,我们应该怎么办呢?
常见的解决办法是:将对象 instanceX 提供的服务service 抽象为一个接口 ServiceProvider, 然后让对象instanceClient 通过持有接口ServiceProvider 的实例来使用服务service 。这种接口间接获得服务的解决方案就是接口模式。
接口模式还可以有一些变化的形式:不止用一个接口抽象一个对象提供的服务,还可用一组就扣抽象一群对象的交互。
接口模式包括:适配器模式,外观模式,合成模式以及桥接模式。
在Java语言中,abstract class 和 interface 是支持抽象类定义的两种机制,那抽象类和接口有什么区别呢?
abstract class 和 interface 之间在对于抽象定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进行抽象类定义是对于 abstract class 和 interface 的选择显得比较随意。其实,两者之间还是有很大的区别的,对于它们的选址甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。
抽象类往往是用来表征我们在问题领域进行分离、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,他们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的,在面向对象领域,抽象类主要用来进行类型隐藏。
使用abstract class 的方式定义 Demo 抽象类的方式如下:
abstract class Demo {
abstract void method1 ();
abstract void method2 ();
}
使用 interface 的方式定义Demo抽象类的方式如下:
interface Daemo {
void method1 ();
void method2 ();
}
在abstract class 方式中,Demo 可以有自己的数据成员,也可以有非数据abstract 的成员方法,而在interface 方式的实现中,Demo 只能够有静态的不能被修改的数据成员(也就是必须static final 的,不过在interface 中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface 是一种特殊形式的abstract class 。 从编程的角度来看,abstract class 和 interface 都可以用来实现 ‘design by contract’(契约式模式)的思想,但是在具体的使用上还是有一些区别的。
abstract class 在Java 语言中表示的是一种继承关系,一个类只能使用一次继承关系(因为Java 不支持多继承)。但是,一个类却可以实现多个interface。在abstract class 的定义中,可以赋予方法的默认行为;但在interface 的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会增加一些复杂性,有时会造成很大的麻烦。
假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open 和 close ,如果要求Door还要具有报警功能,那我们该如何设计这一例子的类结构呢?
使用abstract class 方式定义Door :
abstract class Door {
abstract open ();
abstract close();
}
使用interface 方式定义Door :
interface Door {
void open ();
void close();
}
如果我们直接在Door定义中增加一个方法
在Door的定义中增加一个alarm方法
abstract class Door{
abstract void open ();
abstract void close();
abstract void alarm();
}
或者
interface Door{
void open ();
void close();
void alarm();
}
根据面向对象设计中的一个核心原则ISP,在Door的定义中把Door概念本身固有的行为方法和另一个概念‘报警器’的行为方法混在一起。这样引起的一个问题是哪些仅仅依赖于Door这个概念的模块会因为‘报警器’这个概念的改变而改变,所以以上的方式是不可行的。既然open、close和alarm 属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:
- 两个概念使用abstract class 方式定义
- 两个概念使用interface 方式定义
- 一个概念使用abstract class 方式定义,另一个使用interface 方式定义。
显然,由于Java 语言不支持多重继承,所以两个概念都使用abstract class 方式定义是不可行的。后面两个方式都是可行的。但是对于它们的选择,却反映出对于问题领域中的概念本质的理解,对于设计意图的放映是否正确、合理。
如果两个概念都使用interface 方式来定义,那么反映出两个问题:
- 我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?
- 如果我们 对于问题领域的理解没问题,比如:我们通过对于问题领域的分析发现,AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为两个该闹闹在定义上,均使用interface定义,反映不出上述含义。我们对于问题领域的理解是AlarmDoor 在本质上是Door,同时它有具有报警的功能。继承关系的本质是‘is-a’关系,所以Door我们应该使用abstract class 方式来定义,AlarmDoor 又具有报警器功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface定义。
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(){...}
}
这种实现方式基本上能够明确放映出我们对于问题领域的理解,正确揭示我们的设计意图。abstract class表示的是‘is-a’关系,interface表示的是‘is-like’关系,在选择时,可以作为一个依据。
如果我们认为AlarmDoor在概念本质上是‘报警器’,同时又有Door的功能,那么上述的定义方式就要反过来。
综上所述:
- abstract class 在Java 语言中表示的都是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。
- 在abstract class 中可以有自己的数据成员,也可以有非abstract的成员方法,而在interface中,只能够有静态的不能被修改的(static final)数据成员(不过interface 中一般不定义数据成员),所有的成员方法都是abstract 的。
- abstract class 和 interface 所反映出的设计理念不同,其中 abstract class 表示的是 ‘is-a’关系,interface表示的是‘like-a’的关系。
- 实现抽象类和接口的类必须实现其中的所有的方法,抽象类中可以有非抽象方法,接口中则不能实现方法
- interface中定义的变量默认是public static final型。且必须给其初值,所以实现类中不能重新定义,也不能改变其值。
- abstract class 中的变量默认是friendly型,其值可以在子类中重新定义,也可以重新赋值。
- 接口中的方法默认都是public, abstract 类型。