`

为什么要使用面向接口编程

 
阅读更多

对于各位使用面向对象编程语言的程序员来说,“接口”这个名词一定不陌生,但是不知各位有没有这样的疑惑:接口有什么用途?它和抽象类有什么区别?能不能用抽象类代替接口呢?而且,作为程序员,一定经常听到“面向接口编程”这个短语,那么它是什么意思?有什么思想内涵?和面向对象编程是什么关系?本文将一一解答这些疑问。

1.面向接口编程和面向对象编程是什么关系

首先,面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其一部分。或者说,它是面向对象编程体系中的思想精髓之一。

2.接口的本质

接口,在表面上是由几个没有主体代码的方法定义组成的集合体,有唯一的名称,可以被类或其他接口所实现(或者也可以说继承)。它在形式上可能是如下的样子:

interfaceInterfaceName
{
voidMethod1();
voidMethod2(intpara1);
voidMethod3(stringpara2,stringpara3);
}

那么,接口的本质是什么呢?或者说接口存在的意义是什么。我认为可以从以下两个视角考虑:

1)接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则。体现了自然界“如果你是……则必须能……”的理念。

例如,在自然界中,人都能吃饭,即“如果你是人,则必须能吃饭”。那么模拟到计算机程序中,就应该有一个IPerson(习惯上,接口名由“I”开头)接口,并有一个方法叫Eat(),然后我们规定,每一个表示“人”的类,必须实现IPerson接口,这就模拟了自然界“如果你是人,则必须能吃饭”这条规则。

从这里,我想各位也能看到些许面向对象思想的东西。面向对象思想的核心之一,就是模拟真实世界,把真实世界中的事物抽象成类,整个程序靠各个类的实例互相通信、互相协作完成系统功能,这非常符合真实世界的运行状况,也是面向对象思想的精髓。

2)接口是在一定粒度视图上同类事物的抽象表示。注意这里我强调了在一定粒度视图上,因为“同类事物”这个概念是相对的,它因为粒度视图不同而不同。

例如,在我的眼里,我是一个人,和一头猪有本质区别,我可以接受我和我同学是同类这个说法,但绝不能接受我和一头猪是同类。但是,如果在一个动物学家眼里,我和猪应该是同类,因为我们都是动物,他可以认为“人”和“猪”都实现了IAnimal这个接口,而他在研究动物行为时,不会把我和猪分开对待,而会从“动物”这个较大的粒度上研究,但他会认为我和一棵树有本质区别。

现在换了一个遗传学家,情况又不同了,因为生物都能遗传,所以在他眼里,我不仅和猪没区别,和一只蚊子、一个细菌、一颗树、一个蘑菇乃至一个SARS病毒都没什么区别,因为他会认为我们都实现了IDescendable这个接口(注:descend vi. 遗传),即我们都是可遗传的东西,他不会分别研究我们,而会将所有生物作为同类进行研究,在他眼里没有人和病毒之分,只有可遗传的物质和不可遗传的物质。但至少,我和一块石头还是有区别的。

可不幸的事情发生了,某日,地球上出现了一位伟大的人,他叫列宁,他在熟读、恩格斯的辩证唯物主义思想巨著后,颇有心得,于是他下了一个著名的定义:所谓物质,就是能被意识所反映的客观实在。至此,我和一块石头、一丝空气、一条成语和传输手机信号的电磁场已经没什么区别了,因为在列宁的眼里,我们都是可以被意识所反映的客观实在。如果列宁是一名程序员,他会这么说:所谓物质,就是所有同时实现了“IReflectabe”和“IEsse”两个接口的类所生成的实例。(注:reflect v. 反映 esse n. 客观实在)

也许你会觉得我上面的例子像在瞎掰,但是,这正是接口得以存在的意义。面向对象思想和核心之一叫做多态性,什么叫多态性?说白了就是在某个粒度视图层面上对同类事物不加区别的对待而统一处理。而之所以敢这样做,就是因为有接口的存在。像那个遗传学家,他明白所有生物都实现了IDescendable接口,那只要是生物,一定有Descend()这个方法,于是他就可以统一研究,而不至于分别研究每一种生物而最终累死。

可能这里还不能给你一个关于接口本质和作用的直观印象。那么在后文的例子和对几个设计模式的解析中,你将会更直观体验到接口的内涵。

3.面向接口编程综述

通过上文,我想大家对接口和接口的思想内涵有了一个了解,那么什么是面向接口编程呢?我个人的定义是:在系统分析和架构中,分清层次和依赖关系,每个层次不是直接向其上层提供服务(即不是直接实例化在上层中),而是通过定义一组接口,仅向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类。

这样做的好处是显而易见的,首先对系统灵活性大有好处。当下层需要改变时,只要接口及接口功能不变,则上层不用做任何修改。甚至可以在不改动上层代码时将下层整个替换掉,就像我们将一个WD的60G硬盘换成一个希捷的160G的硬盘,计算机其他地方不用做任何改动,而是把原硬盘拔下来、新硬盘插上就行了,因为计算机其他部分不依赖具体硬盘,而只依赖一个IDE接口,只要硬盘实现了这个接口,就可以替换上去。从这里看,程序中的接口和现实中的接口极为相似,所以我一直认为,接口(interface)这个词用的真是神似!

使用接口的另一个好处就是不同部件或层次的开发人员可以并行开工,就像造硬盘的不用等造CPU的,也不用等造显示器的,只要接口一致,设计合理,完全可以并行进行开发,从而提高效率。

本篇文章先到这里。最后我想再啰嗦一句:面向对象的精髓是模拟现实,这也可以说是我这篇文章的灵魂。所以,多从现实中思考面向对象的东西,对提高系统分析设计能力大有脾益。

下篇文章,我将用一个实例来展示接口编程的基本方法。

而第三篇,我将解析经典设计模式中的一些面向接口编程思想,并解析一下.NET分层架构中的面向接口思想。

对本文的补充:

仔细看了各位的回复,非常高兴能和大家一起讨论技术问题。感谢给出肯定的朋友,也要感谢提出意见和质疑的朋友,这促使我更深入思考一些东西,希望能借此进步。在这里我想补充一些东西,以讨论一些回复中比较集中的问题。

1.关于“面向接口编程”中的“接口”与具体面向对象语言中“接口”两个词

看到有朋友提出“面向接口编程”中的“接口”二字应该比单纯编程语言中的interface范围更大。我经过思考,觉得很有道理。这里我写的确实不太合理。我想,面向对象语言中的“接口”是指具体的一种代码结构,例如C#中用interface关键字定义的接口。而“面向接口编程”中的“接口”可以说是一种从软件架构的角度、从一个更抽象的层面上指那种用于隐藏具体底层类和实现多态性的结构部件。从这个意义上说,如果定义一个抽象类,并且目的是为了实现多态,那么我认为把这个抽象类也称为“接口”是合理的。但是用抽象类实现多态合理不合理?在下面第二条讨论。

概括来说,我觉得两个“接口”的概念既相互区别又相互联系。“面向接口编程”中的接口是一种思想层面的用于实现多态性、提高软件灵活性和可维护性的架构部件,而具体语言中的“接口”是将这种思想中的部件具体实施到代码里的手段。

2.关于抽象类与接口

看到回复中这是讨论的比较激烈的一个问题。很抱歉我考虑不周没有在文章中讨论这个问题。我个人对这个问题的理解如下:

如果单从具体代码来看,对这两个概念很容易模糊,甚至觉得接口就是多余的,因为单从具体功能来看,除多重继承外(C#,Java中),抽象类似乎完全能取代接口。但是,难道接口的存在是为了实现多重继承?当然不是。我认为,抽象类和接口的区别在于使用动机。使用抽象类是为了代码的复用,而使用接口的动机是为了实现多态性。所以,如果你在为某个地方该使用接口还是抽象类而犹豫不决时,那么可以想想你的动机是什么。

看到有朋友对IPerson这个接口的质疑,我个人的理解是,IPerson这个接口该不该定义,关键看具体应用中是怎么个情况。如果我们的项目中有Women和Man,都继承Person,而且Women和Man绝大多数方法都相同,只有一个方法DoSomethingInWC()不同(例子比较粗俗,各位见谅),那么当然定义一个AbstractPerson抽象类比较合理,因为它可以把其他所有方法都包含进去,子类只定义DoSomethingInWC(),大大减少了重复代码量。

但是,如果我们程序中的Women和Man两个类基本没有共同代码,而且有一个PersonHandle类需要实例化他们,并且不希望知道他们是男是女,而只需把他们当作人看待,并实现多态,那么定义成接口就有必要了。

总而言之,接口与抽象类的区别主要在于使用的动机,而不在于其本身。而一个东西该定义成抽象类还是接口,要根据具体环境的上下文决定。

再者,我认为接口和抽象类的另一个区别在于,抽象类和它的子类之间应该是一般和特殊的关系,而接口仅仅是它的子类应该实现的一组规则。(当然,有时也可能存在一般与特殊的关系,但我们使用接口的目的不在这里)如,交通工具定义成抽象类,汽车、飞机、轮船定义成子类,是可以接受的,因为汽车、飞机、轮船都是一种特殊的交通工具。再譬如Icomparable接口,它只是说,实现这个接口的类必须要可以进行比较,这是一条规则。如果Car这个类实现了Icomparable,只是说,我们的Car中有一个方法可以对两个Car的实例进行比较,可能是比哪辆车更贵,也可能比哪辆车更大,这都无所谓,但我们不能说“汽车是一种特殊的可以比较”,这在文法上都不通。

———————————————————————————————————————————————————————————————————————————

接口的起源

计算机出现以后,科学家相继开发了多种语言,从smalltalk,Pascal,Basic,C语言,C++,java,.net等等,这些语言的发展步伐,可以看作是从面向过程向面向对象发展的一段历史。很多面向对象的书在介绍自己的历史的时候,都会给读者介绍这一段历史,并鼓吹OO编程是多么多么的优异。问题是,很多刚开始学程序的人根本不知道为什么要有这个转变,他们也很难理解OO语言中的虚函数,接口等概念到底为了什么而提出来。

我们在了解这一段历史以前,首先给大家介绍一个概念——“粒度”,什么是粒度?作者认为所谓粒度其实就是一个程序中使用代码单元的组合尺度,我们可以举一个例子,沙砾——砖块——房屋模板,我们设想去修建一座房子,其实有很多中修筑方法,如果你不闲麻烦,可以使用沙砾一点点地建筑,或者将沙砾烧制为砖块,用砖块来砌,甚至直接从工厂购买房屋的门,窗,墙组件来堆砌。这三种不同的方法代表了三种不同的组合尺度。沙砾是最小的单位,使用它搭建小的房子说不定还可以,但是毫无疑问,我们必须使用很多很多“沙砾”,不便于管理;砖块比沙砾聚合了一层,可以用来修建较大的房子了;房屋模板是最高的尺寸,使用它可以快速地搭建大规模的房屋。这三种尺度的区别和联系,与我们编写程序概念是有很大的相似之处的。

在早期学习Pascal,老师告诉我们这种面向过程语言的最基本的单元是过程和函数,它们是程序中的最小的组件。过程和函数可以实现最基本的代码重用,当我们把某些固定功能的代码使用过程和函数编写后,我们可以在程序中调用它们而不必在任何需要的地方都写上这样一段代码,这样的好处是显而易见的。在一些小型的程序里面,使用过程和函数是合适的,但是在大中型程序中,它们的弊端就显示出来,过程和函数的粒度太低了,如果我们一个系统,有10000个函数和过程,我们的程序员将不得不花费大量的时间去寻找和维护它们,10000个没有任何关系的函数和过程的管理难度是显而易见的,就好像10000个人的企业一样,如果没有部门和职务,这还不乱了套?!

面向对象语言的出现,就是为了解决这个问题,别听OO语言吹的天花乱坠,其实它出现就为一个理由——提高编程的粒度。面向对象语言的基本单位是类 CLASS,类封装了很多数据成员和成员函数,过程,将最小组件的提高了一个等级,我们需要直接操作的不是过程和函数了,而是一个个更高层次上的类。我们把10000人分了很多部门,不同的部门负责不同的事宜,这样公司终于可以走上正轨了。

做成了类CLASS是否就万事大吉了呢?不一定,新的问题随之而来,也许我们有一个部门的人很多,可以做很多事情,如何在部门内部实现更好的管理呢?好比我们有一个类,它提供了很多种方法和属性,这些方法和属性其实可以分为一堆堆,为不同的功能服务,但是我们的类并没有做这个管理。在AO种,map对象拥有很多功能,比如管理图层,管理元素,管理选择集,进行地图显示,每种不同的功能都有好多方法和属性,现在这些属性和方法是杂乱无章,没有任何区别堆积在一个类里面的,当我们的程序员需要寻找一个方法的时候,不得不一个个去寻找,很不方便。

这个时候,接口interface出现了,C++的发明者第一次提出纯虚函数(其实就是接口)概念的时候,遭到了很多抵制,很多人都不明白接口的意义何在,我们用虚函数好好的,何必又出来个啥东西都没有的空架子?说它是类吧,又不能实现产生一个对象;说它是对象吧,又没有方法体来使用。接口出来干了一件好事,就是将类内部进行分类。对于map对象,我们可以做好几个接口,这些接口中定义不同功能的方法,函数和属性,map类实现这些接口,这样我们可以使用接口定义,实现对象。因此,所谓接口就是一系列相关方法与属性集合的定义。

Dim pGraphicsContainer as iGraphicsContainer
pGraphicsContainer=application.document.ActiveView.focusMap
pGraphicsContainer可以使用的属性和方法就只能是它定义的那部分了,而不能使用管理元素等的接口定义的方法和属性,那我们如何使用其它的功能呢?这就是所谓的QI(Query Interface)功能。从一个接口查询另一个接口。
Dim pGeoFeatureLayer as iGeofeatureLayer
pGeoFeatureLayer= pGraphicsContainer       QI

好了,我们罗嗦了这么多,已经走进了COM的概念了,在正式介绍COM以前我们得最后罗嗦一点:计算机语言的发展历史,其实就是一部不断提高组件粒度的历史,不断提高代码重用的历史。以前我们使用过程和函数,后来我们使用类,现在我们使用接口,都是为了一个目的,让我们操作的组件在具体和抽象之间寻找一个平衡点。太具体了,如过程和函数,就没有了框架;太抽象,如类,就无法分别。

一个代码示例:
public interface IForm
{
    void Show();
    void ShowDialog();
}

public class A:IForm
{
    public void Show()
{
}
    public void ShowDialog()
{
}
}

public class B:IForm
{
    public void Show()
{
}
    public void ShowDialog()
{
}
}


public class FormFactory
{
   public static IForm CreateInstance(string parm)
   {
      
      if  (parm == "A")
      {
         return new A();
      else if (parm == "B")
         return new B();
       }
      return null;
   }
}

==============================================================================

选择Java接口还是抽象类

很多人有过这样的疑问:为什么有的地方必须使用接口而不是抽象类,而在另一些地方,又必须使用抽象类而不是接口呢?或者说,在考虑Java类的一般化问题时,很多人会在接口和抽象类之间犹豫不决,甚至随便选择一种。
实际上接口和抽象类的选择不是随心所欲的。要理解接口和抽象类的选择原则,有两个概念很重要:对象的行为和对象的实现。如果一个实体可以有多种实现方式,则在设计实体行为的描述方式时,应当达到这样一个目标:在使用实体的时候,无需详细了解实体行为的实现方式。也就是说,要把对象的行为和对象的实现分离开来。既然Java的接口和抽象类都可以定义不提供具体实现的方法,在分离对象的行为和对象的实现时,到底应该使用接口还是使用抽象类呢?
通过抽象类建立行为模型
在接口和抽象类的选择上,必须遵守这样一个原则:行为模型应该总是通过接口而不是抽象类定义。为了说明其原因,下面试着通过抽象类建立行为模型,看看会出现什么问题。
假设要为销售部门设计一个软件,这个软件包含一个“发动机”(Motor)实体。显然无法在发动机对象中详细地描述发动机的方方面面,只能描述某些对当前软件来说重要的特征。至于发动机的哪些特征是重要的,则要与用户(销售部门)交流才能确定。
销售部门的人要求每一个发动机都有一个称为马力的参数。对于他们来说,这是惟一值得关心的参数。基于这一判断,可以把发动机的行为定义为以下行为。
行为1:查询发动机的马力,发动机将返回一个表示马力的整数。
虽然现在还不清楚发动机如何取得马力这个参数,但可以肯定发动机一定支持这个行为,而且这是所有发动机惟一值得关注的行为特征。这个行为特征既可以用接口定义,也可以用抽象类定义。为了说明用抽象类定义可能出现的问题,下面用抽象类建立发动机的行为模型,并用Java方法描述行为1,代码如下:
代码
public abstract Motor{
abstract public int getHorsepower();
}

在Motor抽象类的基础上构造出多种具体实现,例如A型发动机、B型发动机等,再加上系统的其它部分,最后得到1.0版的软件并交付使用。一段时间过去了,现在要设计2.0版的软件。在评估2.0版软件需求的过程中,发现一小部分发动机是电池驱动的,而电池需要一定的充电时间。销售部门的人希望能够通过计算机查阅充电时间。根据这一要求定义一个新的行为,如图1所示。
行为2:查询电驱动发动机的充电时间,发动机将返回一个表示充电时间的整数。
用Java方法来描述这个行为,代码如下:
代码
public abstract BatteryPoweredMotor extends Motor{
abstract public int getTimeToRecharge();
}

在销售部门的软件中,电驱动发动机也以类的形式实现,但这些类从BatteryPoweredMotor而不是Motor派生。这些改动加入到2.0版软件之后,销售部门很满意。随着业务的不断发展,不久之后光驱动的发动机出现了。销售部门要求光驱动发动机需要一定光能才能运转,光能以流明(Lumen)度量。这个信息对客户很重要,因为下雨或多云的天气里,某些光驱动发动机可能无法运转。销售部门要求为软件增加对光驱动发动机的支持,所以要定义一个新的行为。
行为3:查询光驱动发动机能够正常运转所需要的最小流明数,发动机返回一个整数。
再定义一个抽象类并把行为3转换成Java方法,代码如下:
代码
public abstract SolarPoweredMotor extends Motor{
abstract public int getLumensToOperate();
}

如图1所示,SolarPoweredMotor和BatteryPoweredMotor都从Motor抽象类派生。在整个软件中,90%以上的代码以相同的方式对待所有的发动机。偶尔需要检查一下发动机是光驱动还是电驱动,使用instanceof实现,代码如下:
代码
if (instanceof SolarPoweredMotor){...}
if (instanceof BatteryPoweredMotor){...}

无论是哪种发动机,马力这个参数都很重要,所以在所有派生的抽象类(SolarPoweredMotor和BatteryPoweredMotor)中,getHorsepower()方法都有效。
现在销售部门又有了一种新的发动机,它是一种既有电驱动又有光驱动的双重驱动发动机。光驱动和电驱动的行为本身没有变化,但新的发动机同时支持两种行为。在考虑如何定义新型的光电驱动发动机时,接口和抽象类的差别开始显示出来了。新的目标是在增加新型发动机的前提下尽量少改动代码。因为与光驱动发动机、电驱动发动机有关的代码已经过全面的测试,不存在已知的Bug。为了增加光电驱动发动机,要定义一个新的SolarBatteryPowered抽象类。如果让SolarBatteryPowered从Motor抽象类派生,SolarBatteryPowered将不支持针对光驱动发动机和电驱动发动机的instanceof操作。也就是说,如果查询一个光电驱动的发动机是光驱动的,还是电驱动的,得到的答案是:都不是。
如果让SolarBatteryPowered从SolarPoweredMotor(或BatteryPoweredMotor)抽象类派生,类似的问题也会出现,SolarBatteryPowered将不支持针对BatteryPoweredMotor(或SolarPoweredMotor)的instanceof操作。从行为上看,光电驱动的发动机必须同时从两个抽象类派生,但Java语言不允许多重继承。之所以会出现这个问题,根本的原因在于使用抽象类不仅意味着定义特定的行为,而且意味着定义实现的模式。也就是说,应该定义一个发动机如何获得行为的模型,而不仅仅是声明发动机具有某一个行为。
通过接口建立行为模型
如果用接口来建立行为模型,就可以避免隐含地规定实现模式。例如,前面的几个行为改用接口定义如下。
行为1:
代码
public interface Motor(){
public int getHorsepower();
}

行为2:
代码
public interface BatteryPoweredMotor extends Motor(){
public int getTimeToRecharge();
}


行为3:
代码
public interface SolarPoweredMotor extends Motor{
abstract public int getLumensToOperate();
}

现在光电驱动的发动机可以描述为:
代码
public DualPoweredMotor implements SolarPoweredMotor, BatteryPoweredMotor{}

DualPoweredMotor只继承行为定义,而不是行为的实现模式,如图2所示。
在使用接口的同时仍旧可以使用抽象类,不过这时抽象类的作用是实现行为,而不是定义行为。只要实现行为的类遵从接口定义,即使它改变了父抽象类,也不用改变其它代码与之交互的方式。特别是对于公用的实现代码,抽象类有它的优点。抽象类能够保证实现的层次关系,避免代码重复。然而,即使在使用抽象类的场合,也不要忽视通过接口定义行为模型的原则。从实践的角度来看,如果依赖于抽象类来定义行为,往往导致过于复杂的继承关系,而通过接口定义行为能够更有效地分离行为与实现,为代码的维护和修改带来方便。

==============================================================================

Java接口特性

在Java中看到接口,第一个想到的可能就是C++中的多重继承和Java中的另外一个关键字abstract。从另外一个角度实现多重继承是接口的功能之一,接口的存在可以使Java中的对象可以向上转型为多个基类型,并且和抽象类一样可以防止他人创建该类的对象,因为接口不允许创建对象。
interface关键字用来声明一个接口,它可以产生一个完全抽象的类,并且不提供任何具体实现。interface的特性整理如下:
1. 接口中的方法可以有参数列表和返回类型,但不能有任何方法体。
2. 接口中可以包含字段,但是会被隐式的声明为static和final。
3. 接口中的字段只是被存储在该接口的静态存储区域内,而不属于该接口。
4. 接口中的方法可以被声明为public或不声明,但结果都会按照public类型处理。
5. 当实现一个接口时,需要将被定义的方法声明为public类型的,否则为默认访问类型,Java编译器不允许这种情况。
6. 如果没有实现接口中所有方法,那么创建的仍然是一个接口。
7. 扩展一个接口来生成新的接口应使用关键字extends,实现一个接口使用implements。
interface在某些地方和abstract有相似的地方,但是采用哪种方式来声明类主要参照以下两点:
1. 如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。
2. 如果知道某个类应该是基类,那么第一个选择的应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。因为抽象类中允许存在一个或多个被具体实现的方法,只要方法没有被全部实现该类就仍是抽象类。
以上就是接口的基本特性和应用的领域,但是接口绝不仅仅如此,在Java语法结构中,接口可以被嵌套,既可以被某个类嵌套,也可以被接口嵌套。这在实际开发中可能应用的不多,但也是它的特性之一。需要注意的是,在实现某个接口时,并不需要实现嵌套在其内部的任何接口,而且,private接口不能在定义它的类之外被实现。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics