扩展接口间的关系
Dave Bartlett ([email protected]) 顾问、作家兼讲师
2001 年 7 月
“对象管理组织(OMG)”的“接口定义语言”已经能使开发人员根据继承创建对象关系。然而,当处理复杂设计时,经常需要支持包含多个接口的对象,并且这些对象是通过组合而不是通过继承来构造的。本周,Dave Bartlett
通过扩展“组件实现定义语言”与标准化定义组件接口的方式,解释了“CORBA 组件模型”如何实现这种需要。
在开始讨论软件组件之前,有必要定义它们是什么以及为什么我们希望使用它们。然后,讨论“组件实现定义语言(CIDL)”以及
CIDL 如何使组件式软件成为现实。
为什么使用软件组件?
为什么我们要使用软件组件?我们的汽车是由零部件(组件)组成;为什么我们喜欢这样,为什么零部件(组件)是重要的?汽车使用零部件(组件)是因为当零部件(组件)用坏或发现有缺陷时,很容易替换掉它(比如轮胎)。这有助于管理变更,我认为这是使用组件的首要原因。在我的汽车里,我不能保持全部应用最新的技术,每一个组件隔离了该项技术的某个方面。这导致了使用组件的第二个原因,管理复杂性。分而治之的策略简化了系统中大量的功能。最后一个原因是重用。为什么重用是最后一个?因为我从来没有真正尝试拆掉我妻子汽车的交流发电机而将它用到我的汽车上,但我猜想我可能会这样做。这可能需要某种层次的式样更新。重用是一个宏伟的目标,但要达到这个目标却格外困难。如果系统易于维护并且随着环境变化和技术改进易于升级系统,那么从长远来看,该系统的价值将会极大地增加。脑子里有了这些目标之后,让我们创建一些定义。
组件是象这样的结构化软件单元,它们:
-
协同定位函数和数据之间的相关性,所以创建结合
-
封装软件单元以减少耦合,并将组件用户与数据存储细节或功能实现隔离开
- 提供唯一的标识,不考虑状态
基于组件的软件的目标是创建高度结合的、合作良好的部件,但各自有明显不同的任务,以及创建系统中组件间低耦合性。这需要系统设计者从对象规范来扩展面向对象原则到用接口定义明确地表示对象相关性。这种组件能力的规范可以划分成跨几个接口。将组件规范划分成多个接口可以使组件内相关性被限制到个别接口,而不是整个组件规范。
例如,提供时间与日期函数的简单组件也必须实现几个接口,这些接口使得该组件在组件容器中能合作工作。对组件容器的更改需要对那些接口进行更改,但不需要更改时间与日期函数。反之亦然:可以不改变接口而更新时间与日期函数,这提供了组件的维护功能。
总之,基于组件的系统是不同的,因为它将规范和实现分离开,还因为将组件规范分成了表示组件和容器角色的一些具体接口。
组合接口
建立接口之间的关系和建立实现类之间的关系是组件设计的基本行为。设计这些关系的两种方式是继承和组合。“Gang of Four”这本设计模式书(请参阅参考资料)的第一章通过对对象、实现和重用的广泛描述展示了对象编程的精髓。这一章明确地提出了使重用成为现实的困难性。继承是面向对象语言典型的特征之一。它也是大多数学生能快速和有效掌握的事物。然而,继承确实有它的不利之处,而对于这些,学生直到具有更多面向对象的经验或除非受益于具有理性意识的导师时,才能意识到。
在 OO 开发中,继承出现在两个不同的阶段。一方面,接口继承(也称作为子类型),它描述当一个对象用来替换另一个对象时。在 C++
中,通过从纯抽象类(具有纯虚拟成员函数的类)公共地继承来做到这一点。由于 Java 编程语言有 interface
关键字,很容易发现接口继承。另一方面,类继承,它依照另一个对象的实现定义对象实现。类继承也称为实现继承。所以,虽然一些编程语言不支持接口继承和实现继承之间的差别,但优秀的程序员在实践中可以区分这两者。例如,C++
通过由抽象类定义类型来处理这两者的差别。
许多设计模式取决于接口继承和实现继承之间这种微妙的差别。Observer
设计模式经常用抽象类(它们是纯接口)来实现。Composite 模式是用来定义公共实现,而这个组合中的组件定义公共接口。
OMG IDL 总是允许您创建基于继承的对象关系。然而,很多时候,我们的设计需要支持包含多个接口的对象,而这些接口是通过组合而不是通过继承来构造的。对象继承允许您依照另一个类来定义这个类的实现,而对象组合允许您通过将对象集合或组合在一起来定义一个类。OMG IDL
需要表达组合和继承的能力。
OMG 已经认识到这些缺陷,并且一直在尝试解决它们。在 1996 年,OMG 发布了 RFP(Request for Proposal)。RFP
的需求将解决对多个接口的需要。CORBA Component Model 被认为是完成最初在这个建议下提出的需求。
“是”对“具有”
当通过继承将子类和超类联系起来时,我们可以说子类具有了超类的一些特征。例如,狗子类“是”犬科超类的代表,可以强制转化成犬科类。“具有”关系是有很大区别的,它表示组合。
一个重要目标是使用标准接口,针对基于开放标准的组件,也许已经定义了这些接口。播放盒式磁带和 CD
的 stereo 组件都有相似的接口,所以我们可以将它抽出来形成它自己的接口。这意味着有一个
CassetteDeck 和 CDPlayer 都使用的 AudioPlayer 接口。AudioPlayer
类似于清单 1 中所示。
清单 1. AudioPlayer 接口
typedef sequence<octet> SoundBytes;
typedef int Speed;
interface AudioStreamIterator {
void getStream(out SoundBytes nextSound,
out boolean hasMore);
};
interface AudioPlayer {
attribute string name;
AudioStreamIterator getSoundStream(in String strSound);
int play(in AudioStreamIterator as);
int fastForward(in Speed spd);
int rewind(in Speed spd);
};
component CassettePlayer supports AudioPlayer{
// The facet for Client components.
provides AudioPlayer audioPort;
};
component CDPlayer supports AudioPlayer{
// The facet for Client components.
provides AudioPlayer audioPort;
};
component Stereo {
attribute string name;
// The receptacles for Cassettes or CDPlayers
uses AudioPlayer Cassette;
uses AudioPlayer CD;
};
|
AudioPlayer 接口有四种处理模糊定义类型的基本方法。SoundByte
是八位元的序列。这意味着它是一个不知道长度的字节流,并且由于它是一个八位元,将不会排列它。getSoundStream()
将返回 AudioStreamIterator 对象,该对象将 SoundByte 装入其中。由于声音文件可以是很庞大的,我们可以将这个序列分割成多个部分,从而使
RMI(远程方法调用)免于陷于困境。play() 方法将 AudioStreamIterator 作为“in”参数。fastForward()
允许您快速前进至流的某个地方,rewind() 使您倒回至流前面的某个地方。
这就是我们的接口。component 关键字是 CIDL 的添加物之一。component 允许 IDL
设计者在其主体内包含该组件的任何属性声明,以及连同一起的组件平面(组件暴露给外界的接口)和容器(组件使用的接口)的声明与该组件可能需要的任何事件的源和接收器。然而,请注意,方法中不允许包含
component 关键字。这是因为我们不是在创建接口 - 我们是在组合接口来形成“组件”。CDPlayer
和 CassettePlayer 组件提供了 AudioPlayer 接口,但 Stereo
组件是作为客户机工作,因为它使用该组件的 CDPlayer 和
CassettePlayer 。由于有了这个 CCM 的添加物和新的 IDL 关键字,您在创建关系时比以前有更大的灵活性。
AudioPlayer
导航和等价接口
所有平面、容器、事件源、事件接收器和属性的声明都映射到生成等价接口(CIDL
规范用这个术语来称谓)的操作。我必须承认我感到自己正走在刀刃上,因为我没有可以使我能检测规范和 IDL 的
CCM 和 CIDL 的实现。我的理解是 CORBA 3.0 CIDL 编译器会为您从组件定义自动生成等价接口。所以,CDPlayer
组件有类似于清单 2 所示的等价接口。
清单 2. CDPlayer 接口
interface CDPlayer : Components::CCMObject, ManagedObject{
AudioPlayer provide_audioPort;
};
|
等价接口中的操作允许组件的客户机检索其平面的引用。除此之外,所有组件从组件基本接口 CCMObject
那继承,该基本接口提供了带一般导航操作(通过 Components::Navigation
接口)的等价接口。这个导航提供类似于清单 3 所示的功能。
清单 3. Components::Navigation 功能
module Components {
valuetype FacetDescription {
public CORBA::RepositoryId InterfaceID;
public FeatureName Name;
};
valuetype Facet:FacetDescription {
public Object ref;
};
typedef sequence<Facet> Facets;
typedef sequence<FacetDescription> FacetDescriptions;
exception InvalidName{};
interface Navigation {
Object provide_facet(in FeatureName name)
raises (InvalidName);
FacetDescriptions describe_facets();
Facets provide_all_facets();
Facets provide_named_facets (in NameList names)
raises (InvalidName);
boolean same_component(in Object ref);
};
};
|
生成等价接口是用来提供组件接口导航。所有组件接口都会继承它。如果您熟悉 COM 世界,这会使您想起来自
IUnknown 的 QueryInterface() 和在 CoCreateInstanceEx()
中使用的 Multi_QI 结构。通过该导航提供类似以下的行为:
-
provide_facet() 方法返回由 name 参数表示的平面的引用,或者如果没有发现由那个 name 参数表示的平面,则返回
InvalidName 异常。
-
describe_facets() 操作返回由组件提供的所有平面的序列。返回的值类型 FacetDescription
将包含 RepositoryId 和该平面的名称。
-
provide_all_facets() 类似于 describe_facets ,但返回的值类型现在将包含支持每个平面的对象的引用。
-
provide_named_facets() 将取一列名称,并返回包含 names 参数中平面的描述和引用的序列(在结构上与
provide_all_facets() 中返回的结构是一致的)。
-
same_component() 将允许客户机决定是否两个引用属于同一个组件实例。该方法是组件实现相关。
结束语
下个月,我将继续研究 CCM 中的 CIDL 构造。对于基于 CORBA 的系统,IDL 是基本的构建技术,将这一点牢记在心中是很重要的。对
IDL 的更改引起了规范和 CORBA 社团的共鸣。添加来创建 CIDL 的关键字是相当广泛和直接明了。很明显,OMG
的梦想是使 CIDL 足够直接明了,以详述软件组件之间的各种关系,以及自动生成许多重现函数和服务用法模式。通过扩展
CIDL,OMG 维护了其语言和平台的中立,但推动了结合坚实工程原则的软件标准。
参考资料
关于作者
Dave Bartlett 居住在美国宾夕法尼亚州的 Berwyn,他是顾问、作家兼讲师。他是
Hands-On CORBA with Java 的作者,这是一本适用于公共课程或企业内部培训的 5
日教程。目前,Dave 正将课程资料编成书,书名是
Thinking in CORBA with Java。Dave 拥有宾州大学的工程和商业硕士学位。如果您对某个主题还有疑问或感兴趣,可通过
[email protected] 与 Dave 联系。 |
|