吴秀祥的博客

软件之美在于她的外在功能、内部结构和团队创建她的过程。
  首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

理解基于接口编程

Posted on 2005-05-25 13:21  吴秀祥  阅读(584)  评论(0)    收藏  举报

Ted Pattison

Microsoft 公司

一九九九年一月

 

注意:本文改编自Ted Pattison所著,Microsoft出版的Programming Distributed Applications with COM&Microsoft Visual Basic(ISBN# 1-57231-961-5)一书 。

摘要:使Microsoft Visual Basic 开发人员理解基于接口的编程。(15页)包括:

介绍

要掌握基于接口编程实在是很艰苦。为了正确认识这种类型的编程,你应该抛弃关于编写代码的旧的习惯和直觉,并且回顾一下过去十年面向对象编程(OPP)和计算机科学的达尔文式的发展,看一看接口是怎样使现代软件设计更适于生存。一个软件要在无时无刻变化的生产环境的丛林中存活下来,它必须有三个明显的特点:可重复使用性,可维护性,可扩充性。这篇文章将给出对基于接口编程的一般性的看法并分析这些特性。

组件对象模型(COM)建立于基于接口编程的思想上,离开接口的概念COM将没有意义。

如果你是一个试图理解COM实际上是怎样工作的Visual Basic 程序员,你应该花时间学习接口是怎样而且为什么在面向对象软件设计中如此重要。对基于接口编程的理解可使你成为一个非常优秀的COM程序员。另外,这使你可以为分布式环境如Microsoft Transaction Server(MTS)创建基于COM应用程序。

基于接口编程存在于COM世界之外。它是一种编程规程,基于共用接口同实现的分离。它在如C++和Smalltalk之类的语言中被软件工程师当作利器,他们发现使用明确的接口可以使他们的软件,特别是大型应用程序更容易维护和扩充。Java的创造者注意到了基于接口编程的简洁性,并且因此直接在他们的语言中建立了对它的支持。

接口与面向对象编程中的代码重用技术相结合可解决许多问题。这篇文章将讨论其中的一些问题。特别是当你的程序的结构与经典OOP相一致时,一个客户程序可建立其与一个类定义的牢固的依赖。这些依赖可以使在不打破客户程序的情况下维护或扩充此类变得很困难。随着时间的过去,为一个对象改进代码变得令人厌烦或基本不可能。当然,某些问题也与称为实现继承的通常的OOP语言特性相关。这个强大的但经常被错用的特性在相似的依赖问题上是脆弱的,它使应用程序的可维护性和可扩展性大打折扣。即使Visual Basic 不支持实现继承,本篇文章也将讨论它的强大功能和限制以说明一些基于被接口编程创造性地解决的问题。

Visual Basic 5.0版加上了一些用户定义实现的接口。这篇文章将向你展示怎样在一个Visual Basic 应用程序中使用接口。在讨论使用接口的基础之后,本文将演示如何实现多态和在线检查,这些使基于接口编程变得如此强大。

类,对象和用户(程序)

你在理解接口的道路上的第一次止步一定是关于基于接口编程必须解决的问题的考虑。这些问题中的许多不得不涉及到处理类和使用类的用户程序之间的关系。想一想下面的问题:一个用户程序与一个类定义之间的关系是什么?用户若想从使用一个类中获益应该知道关于这个类的什么东西?当一个程序员使用一个类的方法和特性编写代码时在用户程序中建立了什么从属关系?

在一个面向对象的范例中,典型的,用户程序实现一个类的实例。一般是用一个new操作符后跟一个类名创建一个对象。在创建一个对象后,用户程序通过一个基于类的引用的变量访问一组特性和方法,以此来使用这个对象。这里是一个使用基于类类型的变量来访问对象的公共成员的简单的例子:

Dim Dog As CDogSet Dog = New Cdog

' 访问一个特性Dog.Name = "Snoopy"

'调用一个方法Dog.Bark

 

在这个例子中,一个基于类的引用使实例化一个Dog对象并与其通信成为可能。用户程序通过一组作为一个对象的公共接口的公共可访问的特性和方法与这个对象通信。类的作者必须用此公共接口把此对象的功能性告诉给用户程序。这使对象有用。注意公共接口中的方法名和特性名以直接代码方式插入用户程序。将来版本的类为了兑现这些建立在用户程序中的依赖关系必须继续提供相同的成员。

使用类的一个好处是它们允许你重复使用代码以 y high concerted effort order to do it well.。一旦一个类被编写,你就可以把它用于一个应用程序中的许多不同的地方。这样,类使你可以减少或消除一个应用程序中的冗余代码,

同时使代码的维护变得容易。你也可以修改或去掉任何非公共可见的属性和方法。你也可以改变公共的方法实现只要调用方法的语法没有被改变。当一个类中的一个方法实现被改进,任何使用这个类的用户程序将很自然地从变化中获益。

当你修改一个已存在的类定义时,为了避免由于打断了已经建立在原始类定义的用户代码与类的依赖关系而造成的危险,你应该不去改动访问任何公共方法和特性的调用语法。只要你保持公共接口不变,你就可以进行任何改动以改进你的类而不破坏用户程序代码。

这里换一种方法来表述上段的关键点:你一旦公布了一个类的公共接口中的某个特性或方法的标记,你就不要改变或取消它。这意味着你应该在一个计划开始时正确设计公共接口并按规程更新类。这使你可以改进和扩充对象代码而不必重写任何用户程序代码。你可以通过修改小错误和改进方法部件来维护一个类。

维护公共接口的现存成员的规则被删节和抽取,但是在你把新功能性添加到类中的时候你有什么灵活性呢?你能做什么来使你在以后的版本中安全的扩充对象呢?把新的公共方法和工具添加到类中是简单而安全的。旧的用户程序即便不能利用对象的新功能性,它也能像以前一样运行。但是在此类修改后所编写的新用户程序可以利用任何加入到公共接口的成员。这意味着任何时候你都可以在产品环境中安全的更新对象。

当你以某种破坏了一个存在的用户程序的方式改变了一个公共方法的特征时就会在类设计中产生问题。这通常在你发现最初的类设计不完全时发生。例如,设想一个提供了狗翻滚行为的方法。下面的RollOver()方法定义了一个十六位整数参数以便使用户程序在每次调用时指定翻转的具体数目:

' 定义在 CDog 类中的方法

Public Sub RollOver(Rolls As Integer)

' 执行

End Sub

' 用户hard-codes 调用语法

Dim Dog As CDog, Rolls As Integer

Set Dog = New CDog

Rolls = 20000

Dog.RollOver Rolls

 

要是在初始设计中没有正确预期到dog对象要求的变化会怎么样?例如,要是所指定的翻转数超过了一个整数型变量最大可能数值(大约32KB)会怎么样?要是一个用户程序调用此函数而参数是5000会怎么样?为了能处理更大的数你必须把参数类型变为长整数型。这带来了一个十足的设计问题。新的用户程序希望32位整数,但是老一些的用户程序例如刚才那个已经建立起对16位整数的依赖关系。

你只有两个选择。其一是修改函数的特征并且重写所有调用它的用户程序代码,另外就是处理原始设计的限制而让其他保持不变。就像你所看到的,不良的类设计不是导致破坏用户代码就是产生不可扩充的对象。

对这个问题的直观解决办法是在你编写与之冲突用户程序代码之前确定类的公共接口的设计包含全部的特征并且确定下来。但这甚至对最有经验的类设计师来说通常也是不可能的。如果类模型是一个现实世界的从不变化的实体,一个有经验的设计师可以创造出一个强壮的并可长期保持的设计。但是,在许多情况下是不可能预测外部变化会如何影响对对象的公共接口的要求的。一个为一个希望在迅速变化的商业环境中使用许多年的应用程序创建类的设计师是不可能预测到将会需要什么的。如果商业模型经常变化,你的类也必须随之变化。在这种情况下需要可扩充的对象。

软件设计者发现使用基于类的引用会导致用户程序和类之间的一层依赖关系。你可以通过规范设计和对将来需求进行预测来减小这些依赖对可维护性和可扩充性的影响。不要在公共接口中定义方法或是特性,除非你准备永远使用它。大多数有经验的设计者把所有数据特性定义为私有的,并且提供通过公共方法访问对象状态的途径。这避免了任何用户程序对实际类的数据格式的依赖。对于把什么方法定义为公共的要非常小心。任何你标记为私有的成员都可能由于类部件的变化而被改动或移动。当然,你不得不把一些成员定义为公共的,否则这个类就没用了。采用基于类的调用的设计总是限于这些折衷的选择。

实现继承(Implementation Inheritance)

OOP的许多特性都意味着赋予编程者更高级的代码复用。类如C++,Smalltalk和Java等语言提供了实现继承的基本特性,它提供了许多可能的方法在面向对象范例中实现代码复用。一些人更是说只有提供了实现继承,一门语言才能被称为一门真正的面向对象语言。这在软件工业界和学术组织中都引起了热烈讨论。本文将不深究讨论的内容,而是把注意力放在与这个功能强大的特性有关的优点和问题上。

在实现继承中,一个类被定义为复用其他类的代码。这个被复用的类被称为父类。获益于复用的类称为子类。Visual Basic 现在不支持实现继承,所以我将使用一个Java的例子来展示实现继承是怎么样的。看一下下面的Java类Cdog:

// 父类

class CDog

{

// DOG 状态

public String Name;

// DOG 行为

public void Bark()

{/* 方法执行 */}

public void RollOver(int Rolls)

{/* 方法执行 */}

}

 

类CDog含有一个属性和两个方法。假设每一个方法对应一个部件。你可以通过使用部件继承来复用此类的状态和行为。CDog将被用作父类。一个扩展了CDog的子类可以继承父类的所有属性和方法部件。下面的Java 代码说明了实现部件继承所需的语法:

// 子类

class CBeagle extends CDog

{

// BEAGLE 状态

// Name 属性被继承

// 加入color 属性

Public String Color;

// BEAGLE 行为

// RollOver()执行被继承

// Bark()执行被覆盖

public void Bark()

{/* CBeagle-specific 执行 */}

// CBeagle 用加入新方法扩充 CDog

public void FetchSlippers()

{/* CBeagle-specific 执行 */}

}

 

当CBeagle(子类)从CDog(父类)中派生出来,它继承了所有存在的属性和方法部件。这意味着CBeagle可以复用CDog中所定义的状态和行为。你可以通过重载现存的方法来扩展CDog,如Bark(),并且可以在CBeagle中添加方法,如FetchSlippers()。你也可以在子类中添加新的属性。

 

19_1.gif (4893 bytes)

图一,部件继承允许一个类复用其他类的状态和行为。子类继承父类的属性和方法部件并且通过重载方法和添加属性和方法来对父类进行扩充。

你应该只在当子类和父类间存在“是一个”的逻辑关系的时候使用实现继承。在这个例子中,就像图一中所示你可以说“一个beagle是一个dog”。当“是一个”的条件满足时,实现继承对于实现代码复用是有用的。当一个应用程序含有许多必须实现一个相同行为的类的时候部件继承有特殊的效用。许多类的通用部分可以提出到一个父类中。例如,一旦类CDog被编写好,它就可以被CBeagle,CTerrier,CBoxer,和其它任何“是一个”dog的类扩充。在CDog类中为定义状态和行为所写的代码可以在许多其他类重复用。

图二展示了继承层次的图形显示。此层次显示了应用程序中各类间的关系。这个层次关系非常简单,你可以创建其它更加复杂的层次关系。设想一个层次关系,这里CScottie从CTerrier派成,CTerrier从CDog派生,CDog从CMammal派生,CMammal从CAnimal派生。正如你预料的那样,继承层会变得很大而且很复杂。包含五个或更多级别的层在产品代码中是不常见的。

19_2.gif (3356 bytes)

图2.一个继承层显示应用程序中上级类和次级类的关系

 

如果正确地使用实现继承,它也可能是代码维护的一个强大机制。当你在上级类中改进方法的实现时,在继承层下的所有类将能自动地从这种改进中受益。改进CAnimal类中的一个错误可以潜在地改进了上百个其他类。随着继承层越来越大越来越复杂,在顶层对类的修改会对下层的许多类产生重大的影响。这意味着一个该动可能会影响许多截然不同的对象类型的行为。

什么是多态性?

到现在为止,本文已经介绍了实现继承怎样提供方法实现的绝对重用性,这种重用产生了更大的可维护性,因为它删除了许多重复代码。实现继承还提供了另一个强大的OPP特性称为多态性。可以说,多态性是面向对象编程中最重要的概念。多态性允许客户用同一种方法处理不同的对象,即使它们是由不同的类产生的并且表现出不同的行为。

你可以利用诸如C++和Java等语言通过实现继承来获得多态性。例如,你可以利用一个上级类的引用来和次级类上的调用方法连接。图3显示了客户怎样利用一个Cdog引用和三种不同类型的对象进行通信。Cdog产生的每个次级类都是和一个Cdog引用类型匹配的。因此,当客户要和CBeagle, CRetriever, or Cboxer类型的对象通信时,就可以使用Cdog引用。

19.gif (2884 bytes)

图3.你可以通过利用一个上级类引用和次级类实例进行通信来获得多态性。客户可以利用一个Cdog引用和任何Cdog匹配对象进行通信。

客户可以确信扩展Cdog类的所有类都将提供一个Bark()方法的实现。客户并不关心次级类是否使用由Dog提供的Bark()定义或次级类是否用自己的实现重载了这个方法。客户只是利用在Cdog类中定义的调用语句来简单地调用这个方法。然而,如果每个次级类都提供自己的Bark()实现,那么每个对象类型可能会以自己独一无二的方法对同一请求做出反应。考察如下Java代码:

// method accepts any CDog-compatible object

Public void MakeDogBark(CDog Dog)

{

// different objects can respond differently

Dog.Bark()

}

 

如果利用一个Cbeagle对象调用这个方法,得到结果可能会和利用Cterrier对象调用得到的结果很不同。客户代码知道调用哪个方法但不知道将会怎样实行Bark()方法。调用语句在编译时就被很好地定义了,但是实际的方法实现直到运行时才能确定。多态性是建立在实现代码的动态粘合的概念的基础之上的,动态粘合与静态粘合相反。动态粘合提供一定程度控制的不确定性,正是这种不确定性才使多态性如此的强大。你可以创建基于插入匹配对象的应用程序。如果已经为Cdog类的公共接口写了成千上万行客户代码,你可以很容易用一个Cterrier对象或Cboxer对象来代替一个Cbeagle对象。这样的改变对客户代码的影响很小或没有影响,因为客户代码依赖于Cdog类而不依赖于任何扩展它的类。

和实现继承有关的问题

到现在为止,本文已经介绍了实现继承的两个最大的优点:方法实现的绝对重用性和多态性。但还没有介绍使用实现继承的一些潜在的问题。不幸的是,实现继承会使应用程序更容易受和基于类的引用有关的同一类型的相关性问题的影响。

通过正确使用封装,你可以向客户隐藏实现的详细资料。这允许你能自由地改变类的实现详细资料而不破坏客户代码。实现继承带来的问题是它破坏了非公共成员的封装。提供实现继承的语言除了公共和私有外还提供了一个保护级的可见性。被标记为保护的属性和方法向客户隐藏但从次级类是可以访问的。因此次级类有权访问已经向客户隐藏的实现详细资料。如果你把一个上级类的保护属性和方法硬件编码到一个次级类中,另一层的顽固的依赖性就产生了。

实现继承是一个被称为白盒重用的开发类型的一个例子。固化在白盒重用中的应用程序通常要经历继承层中类之间的紧密耦合。一旦一个次级类使用一个保护属性或方法,你就不能改变上级类的签名或去掉它而不破坏固化到次级类中的从属关系。这就使带有大量继承层的应用程序很脆弱。改变层顶端的类通常需要改变许多次级类。在某些应用程序中,在层顶端改变一个方法签名或属性类型可能会破坏这个层次链下的几十或几百个类。另一方面,冻结关键上级类的公共和保护接口通常会产生一个不能扩展的系统。

在简单类设计的情况下,你必须认真考虑是否给予一个属性或一个方法以保护的可见性。利用实现继承正确设计,需要高层次的专门技术和训练来避免弱化上级类。你应该知道一个类是否将被其他次级类扩展。如果你希望一个类被扩展,那么向次级类封装实现详细资料和向客户封装它们一样重要。

这并不是说实现继承没有用。在合适的开发环境中它是很强大的。最好在更小的,可控的情形下使用它。并不是所有人都能创建一个能根据应用程序的要求而改进的大的继承层,但是大多数有经验的面向对象设计者都能做到这一点。

当第一次引入C++和Smalltalk的时候,OPP的福音传道者吹嘘实现继承是获得代码重用的灵丹妙药。在过去的十年内,实现继承的偶然使用已经阻碍了许多大型系统的发展。知道实现继承最适合用于小系统的有经验的开发者继续寻找更灵活的方法来在大范围内获得重用。特别地,他们寻找在更大的系统中获得没有折中的可扩展性的获得重用的方法。他们需要一个面向对象的重用机制,这种机制适合更大的不断改进的设计。这就促使了被称为对象合成的基于接口的编程和开发类型的产生。

 

将接口从实现中分离出来

对象合成提供另一种方法获得重用性而不需要紧密耦合。对象合成是建立在黑盒重用的基础之上的,其中一个类的实现详细资料是永远不会向客户显露的。客户只知道一个可用的请求(是什么)设置。对象永远不会暴露反应(怎么样)的内部详细资料。

黑盒重用性建立在接口和实现正式分离的基础上。这意味着接口成为一级市民。一个接口就是一个自身定义的独立的数据类型。这是经典OOP的一个发展,在经典OOP中一个公共接口是在类定义中定义的。

谈到这,你可能正在想这不是更加含糊不清了吗?你问你自己,“接口到底是什么?”不幸地,很难为这个全新的编写软件的方法的关键概念提供一个简明的定义。可以有几种方法来介绍接口。然而,接口在软件设计上的含义对一般程序员来说是非常难掌握的。要学会怎样利用接口进行设计通常要花费几个月或几年的时间。

从根本上来讲,接口是一套公共方法签名。它为一套逻辑相关的请求定义调用语法。但是,当接口定义方法签名时,它不能包含任何实现或数据属性。由于提供了一定层次的间接性,接口减弱了类和使用它的客户之间的关系。这意味着为了可用,接口必须被一个或多个类实现。一旦接口已经被一个类实现了,客户就能从这个类创建一个对象并通过一个接口引用和它通信。

你可以利用接口创建一个对象引用而不是对象本身。这很容易理解,因为一个对象需要数据属性和方法实现,而这些是接口不能提供的。由于接口不是一个可创建的实体,所以它是一个抽象的数据类型。而对象只能从被称为具体数据类型的可创建类来产生实例。

从设计的观点来看,一个接口就是一个合同。实现接口的类保证他服务的对象将支持某一类型的行为。更明确地说,一个类必须为每个由接口定义的方法提供实现。当通过一个接口引用和一个对象通信时,客户可以确信对象将向每个在接口中定义的方法提供一个合理的反应。

多于一个的类可以实现同一个接口。接口为每个方法定义了严密的调用语法和不严密的语义。这些不严密的语义在为每个方法确定合适的对象行为时给每个类的作者一些自由度。例如如果Idog接口定义了一个方法叫做Bark(),不同类的作者可以为同一请求提供不同的反应,只要以某种方式补充狗叫的概念。The Cbeagle类可以用一种与Cterrier和Cboxer都不同的方法实现Bark()。这意味着接口提供了获得多态性的机会。接口允许你构建组合插入匹配对象的应用程序,从这点来说,接口和实现继承相似。不过,接口提供插入匹配无须冒耦合紧密的危险,使用实现继承和白盒重用时可能会发生这种情况。

两种继承

继承是一个面向对象的概念,它模拟两个实体之间的“IS A”关系。迄今为止,本文已经用实现继承的术语代替了更一般的继承术语,因为通过一个次级类扩展上级类是分享“IS A”关系的唯一途径。当类实现一个接口时,它也利用一个“IS A”关系。例如,如果类Cbeagle实现接口Dog,那么说小猎犬“IS A”狗是正确的。你可以在需要Idog匹配对象的任何条件下使用Cbeagle对象。

基于接口的编程是建立在第二种形式继承的基础之上的,这种继承被称为接口继承。这意味着继承不需要方法实现的重用。对继承唯一真正的要求是次级类实例必须和被继承的基础类型相匹配。这允许两种形式的继承都能获得多态性。

两种形式的继承都能提供多态性,但是当他们使用封装时就很不同了。实现继承是建立在白盒重用的基础上的。它允许一个次级类了解它扩展的类的私有信息。这就允许一个次级类经历一个上级类的方法实现和数据属性的绝对重用。实现继承在重用状态和行为方面远比接口继承强大。然而,这一重用也带来了成本。白盒重用中的封装损失限制了它在大型设计中的可伸缩性。

作为一个黑盒重用提出的术语,接口继承增强了封装的概念。严格坚持类内实现详细资料的封装允许设计可伸缩性更好的应用程序。基于接口的编程解决了和白盒重用有关的许多问题。然而,要想欣赏这一类型的编程,你必须接受收益大于成本这一事实。这是许多程序员努力的目标。

当类实现一个接口时,它承担了提供设置方法的义务。次级类作者必须书写额外的代码,不论他们在什么时候决定实现一个接口。当你和实现继承比较时,接口继承看起来好象要做更多的工作。当你从一个类继承时,你的大部分工作已经作好了,但当你从一个接口继承时,你的工作才刚刚开始。打个比方,实现继承看起来闻起来像是一块三明治,而接口继承看起来像是一碗冒着热气的椰菜。你必须抑制对获得三明治的渴望而达到对接口更高层次的了解。接口继承超过实现继承的主要优点是它对折中应用程序可扩展性的紧密耦合不敏感。

通过Visual Basic使用接口

Visual Basic 5.0支持用户自定义接口产品的第一个版本。你可以按如下三个必要的步骤,通过Visual Basic项目获得基于接口编程的好处:

  1. 定义一个接口。
  2. 在一个或多个可创建的类中实现这个接口。
  3. 在客户中利用一个接口引用和对象进行通信。

正如你所见到的那样,向你的应用程序增加接口的基本步骤是相当容易的。使用接口也让你向应用程序设计中加入了多态性。我们将利用一个简单的例子来说明完成这些步骤所需的Visual Basic语法。

利用一个一般类模块在Visual Basic中定义一个自定义接口。如果Visual Basic集成开发环境(IDE)提供一个单独的编辑器用于定义接口会更好,但是不幸的是一个专门用于创建接口的编辑器目前还没有。你利用类模块编辑器来创建接口定义和类。

要定义一个新的接口,只需向一个已经存在的项目加入一个新的类模块。然后你给它一个适当的名字。如果你要创建一个接口来表达一条狗的行为,Idog或itfDog可能是一个比较合适的名字。这是Visual Basic开发者中两个最普通的命名习惯。如果你正在一个ActiveX DLL或ActiveX EXE Visual Basic项目中工作,你应该也将类模块实例属性设置为PublicNotCreatable。这一设置很容易理解,因为接口将代表一个抽象的数据类型。在一个Standard EXE项目中,类模块没有一个实例属性。

你通过为一套公共方法创建调用语法来定义接口。在接口中不要包含任何方法的实现。你只需定义签名,别的就不需要了。本质上,你是在定义客户怎样调用这些方法而不是这些方法怎样实现。

' (class module IDog.cls)

' IDog expresses behavior of a dog object

Public Property Get Name() As String

End Property

Public Property Let Name(ByVal Value As String)

End Property

Public Sub Bark()

End Sub

Public Sub RollOver(ByVal Rolls As Integer)

End Sub

 

在Visual Basic 中声明一个接口的时你首先要注意的是在每个方法签名之后一定要有End Sub, End Function, 或 End Property。这实际上在接口定义中没有什么意义。关键字End通常表示接口实现的结束。这是Visual Basic IDE的一个特性,也是利用类模块同时定义类和接口的一个不幸的负面影响。也许将来版本的Visual Basic将会提供一个模块类型专门用于定义接口,用这种模块类型将不需要End Sub, End Function, 或 End Property,但是现在你必须遵守这一规则。

另外一个重要的注意事项是这个接口除了方法外还可以使用逻辑属性。考虑到逻辑属性实际上是一套方法而不是一种数据属性,所以这是合理的。客户可以利用上述接口中定义的逻辑属性名,就像利用一个一般数据属性一样,但是它必须按照一个Property Let/Property Get方法对来实现。

停下来想一想:接口为什么不能包含数据成员呢?因为接口不像类,是决不能用于创建对象的。它的任务是包装一个类的实现详细资料。对象的数据设计是被封装类定义中最重要的内容。如果一个接口包含实际的数据成员,客户将在他们上面构造相关性。你知道相关性是不好的。

即使接口不能包含数据属性,Visual Basic 仍然允许你在用来定义接口的类模块中定义数据属性,方法如下:

Public Name As String

不过,当你在一个类模块中定义一个数据属性时,Visual Basic透明地重定义数据属性作为一个用于接口定义的逻辑属性。这是创建接口时Visual Basic提供的一个方便。刚才定义的名字属性还要求在任何类中都要有Property Let和Property Get来实现这个接口。注意实现一个接口对一个类定义的数据设计没有影响。实现这个接口的所有类都应该包含一个私有数据属性用于狗名字的物理存储。

创建接口定义后,下一步是创建实现它的类。在你的项目中加入第二个类模块并给它一个适当的名字。例如,你可以创建一个具体的类Cbeagle来实现Idog接口。你必须在类模块的顶部使用关键字Implements。所用的语句和下面的类似:

Implements Idog

一旦一个类模块包含这一行代码,接口中的所有方法和逻辑属性都必须在类模块中有一个相应的实现。Visual Basic编译器将检查这一请求。如果没有提供所有的实现你是不能编译你的代码的。例如,在Idog接口中实现Bark()方法,需要如下定义:

Private Sub IDog_Bark()

' implementation

End Sub

接口的Visual Basic映射要求每个方法实现使用接口名跟一个下划线和方法名而组成的名字。当使用特定的接口时Visual Basic用这个私有属性创建一个到对象的进入点。Visual Basic编译器要求你为接口中的每个方法和逻辑属性提供一个类似的实现。这保证从类创建的对象为每个接口成员提供一个进入点。

幸运的是,如果你在类模块顶部使用关键字Implements ,Visual Basic IDE会很容易为方法实现创建过程存根。类模块的编辑窗口有一个向导条,包含两个下拉combo框。如果你在左面的combo框中选择接口的名字,通过在右面的combo框中选择方法名,你可以很快为方法实现产生框架,如图4所示:

19_3.gif (12377 bytes)

图4 向导条很容易在实现一个自定义接口时创建过程框架。

Implements IDog

Private Name As String

Private Property Let IDog_Name(ByVal Value As String)

Name = Value

End Property

Private Property Get IDog_Name() As String

IDog_Name = Name

End Property

Private Sub IDog_Bark()

' implementation

End Sub

Private Sub IDog_RollOver(ByVal Rolls As Integer)

' implementation

End Sub

 

这个代码样本显示了实现Idog接口的Cbeagle类的部分实现。向导条产生标记为Private的方法实现。这意味着这些方法实现对使用Cbeagle引用的客户来说是不可用的。只有使用Idog引用的客户才能使用他们。上述代码也说明了CBeagle怎样通过定义严格私有数据属性和实现Property Let 和Property Get方法来实现逻辑名字属性。

现在你已经创建了一个接口并创建了一个类来实现它,你可以利用这个接口和一个对象通信。例如,客户可以通过一个Idog引用和一个Cbeagle对象通信。你可以利用Idog引用调用接口显露的所有方法。这里是一个简单的例子:

Dim Dog As IDog

Set Dog = New CBeagle

' access object through interface reference

Dog.Name = "Spot"

Dog.Bark

Dog.RollOver 12

一旦客户通过一个接口引用和对象连接,它就能调用方法并访问逻辑属性。Visual Basic IDE提供和使用基于类的引用时同样的Microsoft IntelliSense?类型检查和调试。注意,你不能在New操作符之后使用接口。接口不是一个可创建的类型。当你使用New操作符时,你必须有一个具体的类如Cbeagle来创建对象。

为什么要使用接口?

当Visual Basic程序员学会怎样在应用程序中使用接口时,他们通常想知道“我为什么要那样做?”或“我为什么要关心?”利用基于类的引用编程和利用自定义接口所需的额外的复杂性比较来看,好象远为自然。如果客户代码利用Cbeagle类的公共方法和属性编程而不是Idog接口,前面的例子会简单得多。自定义接口看起来是没有任何切实利益的额外工作。

有几个重要的原因为什么Visual Basic/COM程序员应该关心接口。第一个原因是接口是COM的基础。在COM中,客户不能使用基于类的引用。而是他们必须通过接口引用访问COM对象。As it turns out,Visual Basic可以将隐藏这一请求的复杂性的工作做的非常好。当你使用一个基于类的引用时,Visual Basic在幕后为类产生一个缺省的COM接口。这意味着你利用Visual Basic创建一个基于COM的应用程序不需要明确地处理自定义接口。然而,如果你增强基于接口的编程,你将成为一个更强的COM程序员。

为什么你应该关心接口的另一个原因是他们可以在软件设计中提供功能和灵活性。当你没有一个类和公共接口之间的一对一映射时,在Visual Basic中使用自定义接口变得很有用。有两个一般情形。在一个情形中,你创建一个接口并在多个类中实现它。在另一个情形中,你在一个类中实现多个接口。两种技术都提供优于客户被限定必须使用基于具体类的应用程序设计的特点。由于基于接口的设计通常需要更多的复杂性,你能利用他们所做的事是有限的。

考虑一种情况,在这种情况中许多类实现同一个接口。例如,假设类CBeagle, CTerrier, 和 Cboxer都实现接口Idog。应用程序可以用如下代码保持Idog匹配对象的一个集合:

Dim Dog1 As IDog, Dog2 As IDog, Dog3 As IDog

' create and initialize dogs

Set Dog1 = New CBeagle

Dog1.Name = "Mo"

Set Dog2 = New CTerrier

Dog2.Name = "Larry"

Set Dog3 = New CBoxer

Dog3.Name = "Curly"

' add dogs to a collection

Dim Dogs As New Collection

Dogs.Add Dog1

Dogs.Add Dog2

Dogs.Add Dog3

应用程序通过用同一种方法处理所有的Idog匹配对象可以获得多态行为。下面的代码说明怎样列举集合和调用每个对象上的Bark()方法:

Dim Dog As IDog

For Each Dog In Dogs

Dog.Bark

Next dog

随着这个应用程序的发展,这个集合可以被修改来保存Idog匹配对象的所有组合,包括从CBeagle, CTerrier, CBoxer,及所有用于实现Idog接口的其他未来的类创建的对象。前面例子中的For Each循环是根据Idog接口编写的,不依赖于任何具体的类。当你向应用程序中引入新的具体的类时,不必修改这个循环。

另一个强大的设计技术是,一个类实现多个接口。如果你这样做的话,你将拥有支持多个接口的对象,因此也支持多个行为。当和运行时类型检查一起使用时,这会变得非常强大。假设这个简单应用程序利用如下方法增加另一个接口IwonderDog:

Sub FetchSlippers()

End Sub

假设Cbeagle类实现IwonderDog但Cterrier类不实现。客户可以在运行时检查一个对象,问它是否支持指定的接口。如果对象确实支持这个接口,客户可以调用它的功能。如果对象不支持这个接口,客户可以比较优雅地降低性能。下面的代码说明怎样利用Visual Basic TypeOf语法为IwonderDog支持进行检测:

Dim Dog1 As IDog, Dog2 As IDog

Set Dog1 = New CBeagle

Set Dog2 = New CTerrier

If TypeOf Dog1 Is IWonderDog Then

Dim WonderDog1 As IWonderDog

Set WonderDog1 = Dog1

WonderDog1.FetchSlippers

End If

If TypeOf Dog2 Is IWonderDog Then

Dim WonderDog2 As IWonderDog

Set WonderDog2 = Dog2

WonderDog2.FetchSlippers

End If

当客户查询Cbeagle对象时,它发现它是IwonderDog匹配的。换句话说,这个对象支持IwonderDog接口。然后这个客户就可以通过利用Set语句casting Idog引用来创建一个IwonderDog引用并分配这个Cbeagle对象给它。一旦客户拥有了一个IwonderDog引用,它就能成功地调用FetchSlippers()。注意,这里有两个引用但只有一个对象。当你有多个接口,客户端的代码会变得更复杂,因为它使用了一个对象的多个引用,以获得全部功能。

当为IWonderDog 匹配性查询Cterrier对象时,客户发现接口不被支持。这允许客户优雅地降低性能。客户端代码能够列举Idog匹配对象的集合并在每个支持IwonderDog接口的对象上安全地调用FetchSlippers(),代码如下:

Dim Dog As IDog, WonderDog As IWonderDog

For Each Dog In Dogs

If TypeOf Dog Is IWonderDog Then

Set WonderDog = Dog

WonderDog.FetchSlippers

End If

Next dog

正如你设想的那样,当你发展一个应用程序时,决定运行时对象的功能的能力是非常有用的。如果以后版本的Cboxer类实现IwonderDog接口,前述代码中显示的For Each循环无须重写就可以利用它。在未来版本的对象中,客户代码可以预见被支持的功能。

扩展对象

前述例子显示怎样利用支持多于一个接口的对象。当现存的一套方法签名已经变得太有限时,你也可以利用自定义接口安全地扩展对象的行为。例如,Idog接口定义RollOver()方法如下:

Public Sub RollOver(ByVal Rolls As Integer)

End Sub

如果你需要扩展应用程序中dog对象的功能以使客户能够传递更大的整型值,你可以创建另一个接口命名为IDog2。假设IDog2接口定义和Idog一样的成员,除RollOver()方法外,这一方法定义如下:

Public Sub RollOver(ByVal Rolls As Long)

End Sub

一个新客户可以进行测试看看Idog对象是否支持新增的行为。如果新增的行为不被支持,客户可以简单地返回到原来的行为。这有一个例子说明客户是怎样工作的:

Sub ExerciseDog(Dog As IDog)

If TypeOf Dog Is IDog2 Then

' use new behavior if supported

Dim Dog2 As IDog2, lRolls As Long

Set Dog2 = Dog

lRolls = 50000

Dog2.RollOver lRolls

Else

' use to older behavior if necessary

Dim iRolls As Integer

iRolls = 20000

Dog.RollOver iRolls

End If

End Sub

The key observation to make about this versioning scheme是你可以向你的应用程序中加入新客户和新对象而不破坏原来的客户和原来的对象。一个新对象通过继续支持原来版本的接口来适应原来的客户。需要时,新客户可以通过使用原来的接口处理原来的对象。在没有接口的环境中,扩展对象通常需要修改所有的客户。修改客户又通常需要修改所有的对象。versioning scheme通过基于接口的编程允许你对应用程序只做很小的改动,而对已经在产品中的代码有很小的影响或没有影响。

 

用自定义接口设计应用程序

本文已经介绍了一个简单的应用程序来说明基于接口编程的核心概念。在实际应用程序中怎样应用这些原理呢?如果要设计一个使用用户对象的大型应用程序,你可以创建自定义接口,Icustomer并且利用接口而不是一个具体的Ccustomer类开始编写大量的客户代码。如果你创建几个类来实现Icustomer接口,你可以获得多态性的即插即用好处。不同类型的用户对象表现出不同的行为,但他们都可以通过同一接口进行控制。

从versioning的角度来看,这一设计允许我们通过向应用程序中引入新的接口来发展各种用户对象的行为。接口例如ICustomer2, ICustomer3, 和 ICustomer4让你安全地扩展用户对象的行为。这种方法最好的方面是你可以独立地修改客户和对象。原来的客户和对象可以使用较早的接口,而较新的客户和对象可以通过较新的接口进行通信。所有这些可以通过接口支持的运行时类型检查来实现。

Microsoft Transaction Server还提供在应用程序设计中使用自定义接口的另一个原因。MTS有一个基于角色的公开安全模型。MTS管理员向MTS角色分配Microsoft Windows NT用户账号和组账号。然后管理员可以在类级和接口级同时配置每个角色对MTS组件的访问许可。你可能会考虑创建一个IcustomerRead和一个IcustomerWrite接口来获得更大程度的粒度。一旦你在Ccustomer中实现了这两个接口,你就可以很容易地配置安全性允许一组用户通过两个接口访问对象,而限定其他用户只能以只读方式访问。自定义接口使MTS安全模型更强大。

总结

由于其他一般技术如使用基于类的引用和实现继承的限制,业界已经采用基于接口的编程方法。自定义接口向应用程序设计和编程带来了一个新层次的复杂性,但是他们的价值是在大型应用程序中易于测量。按照达尔文的观点,基于接口的编程方法使软件更适于生存。接口使你的代码更容易重用,维护和扩展。

COM货真价实是建立在基于对象的编程方法基础之上的。COM要求接口和实现正式分离,也就是说,它要求客户communicate with objects exclusively through interface references。 这就确保了客户决不会在服务于对象的类上建立相关性。这允许COM程序员修正他们的对象代码而不用担心破坏客户代码。COM客户可以从对象获得运行时的类型信息。一个COM客户可以总是查询一个对象并询问它是否支持一个指定的接口。如果被请求的接口不被支持,客户可以发现它并优雅地降低性能。这就允许程序员独立地修改组件和应用程序。原来的客户和原来的对象可以和新客户和新对象和谐地工作。Herein lies the key to versioning in COM.