接口还是抽象类文档格式.docx
- 文档编号:19216902
- 上传时间:2023-01-04
- 格式:DOCX
- 页数:9
- 大小:59.31KB
接口还是抽象类文档格式.docx
《接口还是抽象类文档格式.docx》由会员分享,可在线阅读,更多相关《接口还是抽象类文档格式.docx(9页珍藏版)》请在冰豆网上搜索。
从语法定义层面看abstractclass和interface
在语法层面,Java语言对于abstractclass和interface给出了不同的定义方式,下面以定义一个名为Demo的抽象类为例来说明这种不同。
使用abstractclass的方式定义Demo抽象类的方式如下:
abstractclassDemo{
abstractvoidmethod1();
abstractvoidmethod2();
…
}
使用interface的方式定义Demo抽象类的方式如下:
interfaceDemo{
voidmethod1();
voidmethod2();
}
在abstractclass方式中,Demo可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Demo只能够有静态的不能被修改的数据成员(也就是必须是staticfinal的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。
从某种意义上说,interface是一种特殊形式的abstractclass。
对于abstractclass和interface在语法定义层面更多的细节问题,不是本文的重点,不再赘述,读者可以参阅参考文献〔1〕获得更多的相关内容。
从编程层面看abstractclass和interface
从编程的角度来看,abstractclass和interface都可以用来实现"
designbycontract"
的思想。
但是在具体的使用上面还是有一些区别的。
首先,abstractclass在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。
但是,一个类却可以实现多个interface。
也许,这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。
其次,在abstractclass的定义中,我们可以赋予方法的默认行为。
但是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会增加一些复杂性,有时会造成很大的麻烦。
在抽象类中不能定义默认行为还存在另一个比较严重的问题,那就是可能会造成维护上的麻烦。
因为如果后来想修改类的界面(一般通过abstractclass或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。
但是如果界面是通过abstractclass来实现的,那么可能就只需要修改定义在abstractclass中的默认行为就可以了。
同样,如果不能在抽象类中定义默认行为,就会导致同样的方法实现出现在该抽象类的每一个派生类中,违反了"
onerule,oneplace"
原则,造成代码重复,同样不利于以后的维护。
因此,在abstractclass和interface间进行选择时要非常的小心。
从设计理念层面看abstractclass和interface
上面主要从语法定义和编程的角度论述了abstractclass和interface的区别,这些层面的区别是比较低层次的、非本质的。
本小节将从另一个层面:
abstractclass和interface所反映出的设计理念,来分析一下二者的区别。
作者认为,从这个层面进行分析才能理解二者概念的本质所在。
前面已经提到过,abstarctclass在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"
isa"
关系,即父类和派生类在概念本质上应该是相同的(参考文献〔3〕中有关于"
关系的大篇幅深入的论述,有兴趣的读者可以参考)。
对于interface来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。
为了使论述便于理解,下面将通过一个简单的实例进行说明。
考虑这样一个例子,假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstractclass或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示:
使用abstractclass方式定义Door:
abstractclassDoor{
abstractvoidopen();
abstractvoidclose();
使用interface方式定义Door:
interfaceDoor{
voidopen();
voidclose();
其他具体的Door类型可以extends使用abstractclass方式定义的Door或者implements使用interface方式定义的Door。
看起来好像使用abstractclass和interface没有大的区别。
如果现在要求Door还要具有报警的功能。
我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstractclass和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)?
下面将罗列出可能的解决方案,并从设计理念层面对这些不同的方案进行分析。
解决方案一:
简单的在Door的定义中增加一个alarm方法,如下:
abstractvoidalarm();
或者
voidalarm();
那么具有报警功能的AlarmDoor的定义方式如下:
classAlarmDoorextendsDoor{
voidopen(){…}
voidclose(){…}
voidalarm(){…}
classAlarmDoorimplementsDoor{
这种方法违反了面向对象设计中的一个核心原则ISP(InterfaceSegregationPriciple),在Door的定义中把Door概念本身固有的行为方法和另外一个概念"
报警器"
的行为方法混在了一起。
这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"
这个概念的改变(比如:
修改alarm方法的参数)而改变,反之依然。
解决方案二:
既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。
定义方式有:
这两个概念都使用abstractclass方式定义;
两个概念都使用interface方式定义;
一个概念使用abstractclass方式定义,另一个概念使用interface方式定义。
显然,由于Java语言不支持多重继承,所以两个概念都使用abstractclass方式定义是不可行的。
后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。
我们一一来分析、说明。
如果两个概念都使用interface方式来定义,那么就反映出两个问题:
1、我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?
2、如果我们对于问题领域的理解没有问题,比如:
我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用interface方式定义)反映不出上述含义。
如果我们对于问题领域的理解是:
AlarmDoor在概念本质上是Door,同时它有具有报警的功能。
我们该如何来设计、实现来明确的反映出我们的意思呢?
前面已经说过,abstractclass在Java语言中表示一种继承关系,而继承关系在本质上是"
关系。
所以对于Door这个概念,我们应该使用abstarctclass方式来定义。
另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。
如下所示:
interfaceAlarm{
classAlarmDoorextendsDoorimplementsAlarm{
这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。
其实abstractclass表示的是"
关系,interface表示的是"
likea"
关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:
如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。
结论
abstractclass和interface是Java语言中的两种定义抽象类的方式,它们之间有很大的相似性。
但是对于它们的选择却又往往反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理,因为它们表现了概念间的不同的关系(虽然都能够实现需求的功能)。
1abstractclass是类,interface为多态而产生
2abstractclass是可以有非abstract的方法的,而且有默认的行为
3abstractclass是“is”而interface是“like”
举例二:
很多人有过这样的疑问:
为什么有的地方必须使用接口而不是抽象类,而在另一些地方,又必须使用抽象类而不是接口呢?
或者说,在考虑Java类的一般化问题时,很多人会在接口和抽象类之间犹豫不决,甚至随便选择一种。
实际上接口和抽象类的选择不是随心所欲的。
要理解接口和抽象类的选择原则,有两个概念很重要:
对象的行为和对象的实现。
如果一个实体可以有多种实现方式,则在设计实体行为的描述方式时,应当达到这样一个目标:
在使用实体的时候,无需详细了解实体行为的实现方式。
也就是说,要把对象的行为和对象的实现分离开来。
既然Java的接口和抽象类都可以定义不提供具体实现的方法,在分离对象的行为和对象的实现时,到底应该使用接口还是使用抽象类呢?
通过抽象类建立行为模型
在接口和抽象类的选择上,必须遵守这样一个原则:
行为模型应该总是通过接口而不是抽象类定义。
为了说明其原因,下面试着通过抽象类建立行为模型,看看会出现什么问题。
假设要为销售部门设计一个软件,这个软件包含一个“发动机”(Motor)实体。
显然无法在发动机对象中详细地描述发动机的方方面面,只能描述某些对当前软件来说重要的特征。
至于发动机的哪些特征是重要的,则要与用户(销售部门)交流才能确定。
销售部门的人要求每一个发动机都有一个称为马力的参数。
对于他们来说,这是惟一值得关心的参数。
基于这一判断,可以把发动机的行为定义为以下行为。
行为1:
查询发动机的马力,发动机将返回一个表示马力的整数。
虽然现在还不清楚发动机如何取得马力这个参数,但可以肯定发动机一定支持这个行为,而且这是所有发动机惟一值得关注的行为特征。
这个行为特征既可以用接口定义,也可以用抽象类定义。
为了说明用抽象类定义可能出现的问题,下面用抽象类建立发动机的行为模型,并用Java方法描述行为1,代码如下:
publicabstractMotor{
abstractpublicintgetHorsepower();
}
在Motor抽象类的基础上构造出多种具体实现,例如A型发动机、B型发动机等,再加上系统的其它部分,最后得到1.0版的软件并交付使用。
一段时间过去了,现在要设计2.0版的软件。
在评估2.0版软件需求的过程中,发现一小部分发动机是电池驱动的,而电池需要一定的充电时间。
销售部门的人希望能够通过计算机查阅充电时间。
根据这一要求定义一个新的行为,如图1所示。
行为2:
查询电驱动发动机的充电时间,发动机将返回一个表示充电时间的整数。
用Java方法来描述这个行为,代码如下:
publicabstractBatteryPoweredMotorextendsMotor{
abstractpublicintgetTimeToRecharge();
在销售部门的软件中,电驱动发动机也以类的形式实现,但这些类从BatteryPoweredMotor而不是Motor派生。
这些改动加入到2.0版软件之后,销售部门很满意。
随着业务的不断发展,不久之后光驱动的发动机出现了。
销售部门要求光驱动发动机需要一定光能才能运转,光能以流明(Lumen)度量。
这个信息对客户很重要,因为下雨或多云的天气里,某些光驱动发动机可能无法运转。
销售部门要求为软件增加对光驱动发动机的支持,所以要定义一个新的行为。
行为3:
查询光驱动发动机能够正常运转所需要的最小流明数,发动机返回一个整数。
再定义一个抽象类并把行为3转换成Java方法,代码如下:
publicabstractSolarPoweredMotorextendsMotor{
abstractpublicintgetLumensToOperate();
如图1所示,SolarPoweredMotor和BatteryPoweredMotor都从Motor抽象类派生。
在整个软件中,90%以上的代码以相同的方式对待所有的发动机。
偶尔需要检查一下发动机是光驱动还是电驱动,使用instanceof实现,代码如下:
if(instanceofSolarPoweredMotor){...}
if(instanceofBatteryPoweredMotor){...}
无论是哪种发动机,马力这个参数都很重要,所以在所有派生的抽象类(SolarPoweredMotor和BatteryPoweredMotor)中,getHorsepower()方法都有效。
现在销售部门又有了一种新的发动机,它是一种既有电驱动又有光驱动的双重驱动发动机。
光驱动和电驱动的行为本身没有变化,但新的发动机同时支持两种行为。
在考虑如何定义新型的光电驱动发动机时,接口和抽象类的差别开始显示出来了。
新的目标是在增加新型发动机的前提下尽量少改动代码。
因为与光驱动发动机、电驱动发动机有关的代码已经过全面的测试,不存在已知的Bug。
为了增加光电驱动发动机,要定义一个新的SolarBatteryPowered抽象类。
如果让SolarBatteryPowered从Motor抽象类派生,SolarBatteryPowered将不支持针对光驱动发动机和电驱动发动机的instanceof操作。
也就是说,如果查询一个光电驱动的发动机是光驱动的,还是电驱动的,得到的答案是:
都不是。
如果让SolarBatteryPowered从SolarPoweredMotor(或BatteryPoweredMotor)抽象类派生,类似的问题也会出现,SolarBatteryPowered将不支持针对BatteryPoweredMotor(或SolarPoweredMotor)的instanceof操作。
从行为上看,光电驱动的发动机必须同时从两个抽象类派生,但Java语言不允许多重继承。
之所以会出现这个问题,根本的原因在于使用抽象类不仅意味着定义特定的行为,而且意味着定义实现的模式。
也就是说,应该定义一个发动机如何获得行为的模型,而不仅仅是声明发动机具有某一个行为。
通过接口建立行为模型
如果用接口来建立行为模型,就可以避免隐含地规定实现模式。
例如,前面的几个行为改用接口定义如下。
publicinterfaceMotor(){
publicintgetHorsepower();
行为2:
publicinterfaceBatteryPoweredMotorextendsMotor(){
publicintgetTimeToRecharge();
行为3:
publicinterfaceSolarPoweredMotorextendsMotor{
现在光电驱动的发动机可以描述为:
publicDualPoweredMotorimplementsSolarPoweredMotor,BatteryPoweredMotor{}
DualPoweredMotor只继承行为定义,而不是行为的实现模式,如图2所示。
在使用接口的同时仍旧可以使用抽象类,不过这时抽象类的作用是实现行为,而不是定义行为。
只要实现行为的类遵从接口定义,即使它改变了父抽象类,也不用改变其它代码与之交互的方式。
特别是对于公用的实现代码,抽象类有它的优点。
抽象类能够保证实现的层次关系,避免代码重复。
然而,即使在使用抽象类的场合,也不要忽视通过接口定义行为模型的原则。
从实践的角度来看,如果依赖于抽象类来定义行为,往往导致过于复杂的继承关系,而通过接口定义行为能够更有效地分离行为与实现,为代码的维护和修改带来方便。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 接口 还是 抽象