现在的位置: 首页 > 综合 > 正文

2.2《框架通讯契约——接口》

2012年04月05日 ⁄ 综合 ⁄ 共 5379字 ⁄ 字号 评论关闭

  插件式框架的宿主程序启动以后,它首先会遍历插件文件夹中所有的插件程序集,这些程序集文件以dll格式存在,框架宿主程序将遍历程序集中包含的插件类型,然后由插件引擎负责依据类型分别生成ICommandIToolIToolBarIMenuIDockableWindow等对象,这些对象将放在一个插件对象池中传递给宿主程序进行进一步解析和处理。

一个插件程序集中可以包含多个插件类型(即多个ClassStruct代码文件可以编译在一个Assembly文件中),框架宿主程序如何识别这些类型的对象是否为插件对象呢?这是由于每个插件对象都有一个身份标识──接口,这个标识在框架设计中被称为“通讯契约”──由于接口也可以被看作是一种类型并定义了必要的方法和属性,宿主程序就可以通过这种契约来生成具体的UI对象并对外界操作作出事件反应。宿主程序必须声明自己有什么样的功能可被插件调用,且插件必须符合什么条件才能被宿主程序使用;反之,插件也需要知道宿主程序提供了什么样的功能和属性,才能将自己融入宿主程序的结构中。

插件式框架作为一个高聚合低耦合的系统,它的功能定义与功能实现之间是分离的,这种策略是高聚合实现的保证。插件式框架好像一个功能聚集站,它对外界宣布,只要符合它发布的插件规范的组件(实现接口的类通常称为组件)都可以加入到这个平台中,而这些组件的具体功能,它并不关心,当然,框架也提供了一些必要的信息和帮助保证这些组件能够正常实现它们的功能。

通讯契约可以以接口或抽象类的形式出现,但一般都是采用接口。这是典型的桥接(Bridge)模式。桥接模式是一种被认可的表示定义和与之关联的实现的方式,它和工厂(Factory)模式为所有的应用提供了基础。使用桥接模式开发的组件具有为了一个共同目标而多方协同工作除某些必须实现的接口外无需彼此交互的优点。[1]

在具有多个逻辑层的结构设计中,各层之间的通讯大多通过接口实现,由于接口不会轻易改变,如果一个层的功能发生变化,不会影响其它层;只要实现了接口的组件功能正常,那么程序的运行就没有问题。这种做法使得各层之间的相互影响降低到最低程度。

2.2.1.    接口的秘密

在大部分编程和设计工作中,很少遇到需要考虑“接口(Interface)”这个术语的情况,如果我们仅仅满足通过拖曳控件的方式在IDE上编程和使用.NET Framework中一般的类库,可能永远也不会在程序中使用到接口,即使在C#等面向对象语言的语法书中读者会无数次看到过这个名词。

接口作为类型而存在

接口是类型(Class)一般行为的定义和契约。如所有的交通工具都包含启动、停车、加速和刹车等方法,交通工具类(VehicleClass)可以将这些一般的、公共的方法都定义在一个接口中,而不同类型车辆之间的这些操作可能并不一样,但接口并不考虑它们各自采用什么方式实现自己的这些功能或方式之间的差异,它只关心这些类型都必须实现接口定义的所有功能,而实现了这个接口的类型都可以看作是一种交通工具。

因此,接口的两个主要作用是:

l         定义多个类型都需要的公共方法和属性;

l         作为一种不可实例化的类型存在;

实现了接口的类或结构必须实现接口中定义的每一个属性和方法,这些方法在设计或功能上往往是相关的,这是最典型的接口设计策略。

接口与抽象类

接口与抽象类非常类似,例如两者都不能产生实例对象,但都可以作为一种契约和定义被使用。但接口和抽象类有本质的不同,这些不同包括:

l         接口没有任何实现部分,但抽象类可以包含部分实现代码;

l         接口没有字段,但抽象类可以包含字段;

l         接口可以被结构(struct)继承,但抽象类不行;

l         抽象类有构造函数和析构函数;

l         接口仅仅能继承自接口,而抽象类可以继承自其它类和接口;

l         接口支持多继承而抽象类仅仅支持单根继承。

下面是接口和抽象类的一个对比例子:

接口IIterface

public interface IInterface {

     void MethodA (int a);

     void MethodB (int a);

     void MethodC (int a);

     void MethodD (int a);

}

抽象类AClass

public abstract class AClass {

     abstract public void MethodA (int a);

     abstract public void MethodB (int a);

     abstract public void MethodC (int a);

     abstract public void MethodD (int a);

}

接口不允许包含操作符重载,其原因并非它不能实现,由于接口是一个公共契约,会公布给其它类型语言使用,而如VB.NET这样的语言是不支持运算符重载的,如果在接口中实现运算符重载会引起不同语言之间的兼容性问题。

接口定义的成员也不允许有修饰符,这是因为接口本身就是public的,它要求其成员必须是公有,否则这样的接口就没有任何意义,不被公布的契约是无用的。如果一定要在接口的成员前加上某种修饰符,就会引发编译器错误。

多继承并非接口存在的理由

即使不从桥接模式的观点考虑,许多面向对象语言引进接口这个术语还有另一个重要理由:一个类可以实现多个接口。

在大部分面向对象语言中,多重继承都是被禁止的,这是因为当编译器需要找到虚方法(Virtual Method)的正确实现部分时,会出现不确定性情况,编译器并不知道某个虚方法在执行时该指向哪一个基类的实现方法;但接口并没有实现部分,即使一个类型实现了多个接口,也不会出现不确定的情况。

接口也是一种类型,与抽象类一样,它可以看作是一种无法实例化的类型,因此,通过实现多个接口的方法,可以将一个对象认为是多种类型。例如,如果要对一辆水陆两用汽车进行抽象,在产品登记目录中,是将它归于车辆类呢,还是归于船舶类?显然,单根继承机制很难描述这种结构,我们无法让水陆两用汽车类型同时继承自两个基类;而车辆类和船舶类是没有继承关系的,也不能通过多级继承的方式让这辆水陆两用汽车归于正确的产品目录中去。

使用接口却可以避免这个问题,我们可以定义一个车辆接口(IBusInterface)和一个船舶接口(IShipInterface),而水陆两用汽车类型只需要同时实现这两个接口,这样,它既可以看作是一种车辆类型,也可以看作是一种船舶类型。

在上面的例子中,接口表现出了在替代多重继承方面的重要优势,但是,它是否意味着接口就是为了这个目的而被面向对象语言的设计者提出的呢?

绝非如此,接口的实现并非类似类(Class)的继承,它也并不是多重继承机制的替代品。

我们常常将接口与抽象类的使用方式相混淆,事实上,两者具有本质的不同:接口是用来定义两个对象通信的契约,而抽象类用来封装对象间公用的行为;抽象类应主要用于具有继承关系的类群中,而接口最适合为不相关的类提供互相调的方法。

接口和抽象类设计之初的目标完全不同,但在实际应用中被太多的读者误解。这个差别在许多组件库设计中时有体现:不同类型的组件互相调用是通过接口,而同一类型的组件,如Color类,则是通过抽象类组织在一起的,即接口是对外通讯的,抽象类是对内组织的。

对接口使用的建议

MSDN的相关内容中,给出了如下关于接口与抽象类的建议:

l         如果预计要创建组件的多个版本,则创建抽象类。抽象类提供简单易行的方法来控制组件版本。通过更新基类,所有继承类都随更改自动更新。另一方面,接口一旦创建就不能更改。如果需要接口的新版本,必须创建一个全新的接口。

l         如果创建的功能将在大范围的全异对象间使用,则使用接口。抽象类应主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能。

l         如果要设计小而简练的功能块,则使用接口。如果要设计大的功能单元,则使用抽象类。

l         如果要在组件的所有实现间提供通用的已实现功能,则使用抽象类。抽象类允许部分实现类,而接口不包含任何成员的实现。

此接口非彼接口

COM组件编码经验的开发人员需要注意,C#中的接口与COM接口不同,前者不需要支持任何COM基础结构,例如C#中的接口并非派生自IUnknown接口,它也没有GUID。但它们都具有一个共同的特征,即提供契约而非实现。

2.2.2.    实现接口与显式实现接口

接口的实现看起来非常简单,可以按下面的方式来完成(C#的接口使用了Pascal命名风格,接口前面加上I标识接口):

public interface IA

{

     void Run();

}

public class CA : IA

{

     #region IA 成员

     public void Run()

     {

         System.Console.WriteLine("CA");

     }

     #endregion

}

如果读者使用的是VS2005工具,在实现接口的时候还将弹出一个选择项“实现接口IA或显式实现接口IA”,实现接口看起来很简单,但“显式实现”又是怎么一回事情呢?

我们修改上面的接口定义代码,并添加一个新接口IB

public interface IA

{

     void Run();

}

public interface IB

{

     void Run();

}

两个接口IAIB都有一个同名的Run方法存在,修改代码让类型CA实现它们,其中IB是显式实现:

public class CA : IA,IB

{

     #region IA 成员

     public void Run()

     {

         System.Console.WriteLine("IA.Run");

     }

     #endregion

     #region IB 成员

     void IB.Run()

     {

         System.Console.WriteLine("IB.Run");

     }

     #endregion

}

我们会发现,在CA实现IAIB接口的过程中,如果IA接口被“实现”了,IB接口就只能显式实现,或者IAIB接口均为显式实现,否则就会出现编译错误。现在测试一下CA中的方法,看看有什么区别:

public static void Main()

{

     CA ca = new CA();

     ca.Run();

     IA ia = ca;

     ia.Run();

     IB ib = ca;

     ib.Run();

}

测试结果分别是:

IA.Run

IA.Run

IB.Run

可以看到,如果是直接访问类型,其Run方法是IA中定义的方法而不是“显式实现”的接口IB中定义的方法。

使用“显式实现”的接口方法有如下特点:

l         不能在方法调用、属性访问以及索引指示器访问中通过全权名来访问显式接口成员的代码。显式接口成员执行体只能通过实现了接口的类实例来访问,如在例中只能通过IB接口访问;

l         显式接口成员执行体不能使用任何访问限制符,也不能加上abstract, virtual, overridestatic 修饰符;

l         显式接口成员执行体和其他成员有着不同的访问方式。因为不能在方法调用、属性访问以及索引指示器访问中通过全权名访问,显式接口成员执行体在某种意义上是私有的。但它们又可以通过接口的实例访问,也具有一定的公有性质;

l         只有类在定义时,把接口名写在了基类列表中,而且类中定义的全权名、类型和返回类型都与显式接口成员执行体完全一致时,显式接口成员执行体才是有效的。

一般而言,使用显式接口成员执行体有两个目的:

一是显式接口成员执行体不能通过类的全权名进行访问,这就可以从公有接口中把接口的实现部分单独分离开,如果一个类所有的方法都是显式实现而来,那么这个类将无法通过类实例化的方式来调用其包含的方法,而只能通过接口定义的方式来访问。如果一个类只在内部使用该接口,则类的外部调用者将不能直接接触到该接口,这种显式接口成员执行体就可以只在内部发挥作用。

其次,显式接口成员执行体避免了多个接口成员之间因为同名而发生混淆。如果一个类希望对名称和返回类型相同的接口成员采用不同的实现方式,就必须要使用到显式接口成员执行体。如果没有显式接口成员执行体,那么对于名称和返回类型不同的接口成员,类也无法进行实现。我们可以修改IA中定义的方法,添加一个具有不同返回值和参数的Run函数,这时类型CA实现接口IA就必须采用“显式实现”了。

 

如果一个实现了两个接口,并且这两个接口都包含具有相同签名的方法,在类中实现该方法将导致两个接口使用同一方法作为它们的定义实现。如果两个接口成员执行不同的功能,就可能导致其中一个接口的实现不正确或两个接口的实现都不正确,这种情况也需要显式地实现接口成员。

简而言之,显式实现接口包含了两种能力:产生一个只在一定范围内被使用的契约和为一个类区别它实现的不同接口的相同方法。




[1] Christian Gross,.NET2.0模式开发实战 北京:人民邮电出版社 2007 p57

【上篇】
【下篇】

抱歉!评论已关闭.