0%

官方系统架构设计师教程-ch9-面向构建的软件设计

面向构件的软件设计

面向构件的软件设计是现在软件设计的重要方法,是软件生产线技术、软件工厂技术重要的理论基础。而在20世纪90年代之前,软件领域中的构件一直没有成功发展,这与软件领域中软件复杂度管理和软件复用粒度需求有关。术语“软件构件”的定义以及“构件化软件”等的仍然存在许多不同的观点,但构件概念已经在工程中被建立并且发展较为成熟,本章讨论软件构件的概念及构件化软件开发设计的方法与存在问题。

他这里的构件,给我的感觉更像是docker的容器?
构件开发技术与微服务架构的区别
SOA和微服务
领域建模

构件的概念

人们经常不做区分地使用术语“构件”和“对象”,以及像“构件对象”这样的短语组合。对象常被称为是类的实例,对象和构件都通过类或接口向外界提供服务。对于对象和构件之间的交互,设计人员常用模式描述,用框架规范。而构件和框架又都常被称为是白盒的或黑盒的。程序语言设计者还不断提出诸如名字空间、模块、包等各种名词。这种术语和概念泛滥的现象亟待改变,应该消除它们的冗余歧义,或者对它们进行阐明、解释与辨析。这些都让术语“构件”更加扑朔迷离。

术语与概念

构件技术蕴涵了太多概念。一个典型的例子就是含义众多的术语“对象”。随着时间的推移,模块、类和构件的概念都最终为“对象”所包括。最近,术语“软件构件”甚至到了把以前的普通对象也称作是构件的程度。将几个术语合并为一个,看起来似乎方便了使用,但除此之外再无其他好处。所以,必须在保证术语的准确度和直观性的前提下取得某种平衡。下面定义了几个关键的术语,并描述了它们之间的关系。

构件

构件的特性如下。

● 独立部署单元。

● 作为第三方的组装单元。

● 没有(外部的)可见状态。

这些特性有几重含义。一个构件是独立可部署的,意味着它必须能跟它所在的环境及其他构件完全分离。因此,构件必须封装自己的全部内部特征。并且,构件作为一个部署单元,具有原子性,是不可拆分的。也就是说,第三方没有权利访问其所使用的任何构件的内部细节信息。

在这样的约束下,如果第三方厂商能将一个构件和其他构件组装在一起,那么这个构件不但必须具备足够好的内聚性,还必须将自己的依赖条件和所提供的服务说明清楚。换句话说,构件必须封装它的实现,并且只通过良好定义的接口与外部环境进行交互。

最后,一个构件不能有任何(外部的)可见状态——这要求构件不能与自己的拷贝有所区别。但对于不影响构件功能的某些属性,例如,用于计费的序列号,则没有这种限制。通过对属性的可见性进行限制,允许用户在不影响构件的可见行为的前提下,使用合法的技术手段对那些影响性能的状态进行特殊处理。特别是,构件可以将某些状态专门用于缓存(缓存具有这样的特性:当它被清空时除了可能会降低性能以外,没有其他后果)。

构件在特定的系统中可以被装载和激活。但是,由于构件本质上没有状态,因此,在同一操作系统进程中装载多个构件的拷贝是毫无意义的,而且它们之间是不可区分的。也就是说,给定一个进程(或者其他的语境),至多会存在一个特定构件的拷贝。因此,谈论某个构件的可用拷贝的数量是没有什么意义的。

在目前许多系统中,构件被实现为大粒度的单元,系统中的构件只能有一个实例。例如,一个数据库服务器可以作为一个构件。如果这个服务器刚好只维护了一个数据库,那么会很容易把该数据库误认为是实例,如公司里的员工工资管理服务器。该数据库服务器连同其中的数据库,可以被视为一个有可见状态的模块。根据上面的定义,该数据库并不是一个构件,但那个静态的数据库服务器程序却是一个构件——它只支持一个数据库“对象”实例。也就是说,在这个例子中,工资管理服务器程序是一个构件,而其中的工资数据只是实例(对象)。这种将不易变的“模型”和易变的“实例”分离的做法避免了大量的维护问题。如果允许构件拥有可见状态的话,那么任何两个来自同一个构件的实例都不会拥有相同的属性。

在这一点上一定要分辨清楚。这里所说的构件的概念与对象层次上的可见或不可见状态无关,也与对象状态的生命周期(每次呼叫,每次会话,或是永久的)无关。这些全都是对象层次上所关心的东西,与构件的概念并没有直接的关系,但是通过构件,我们可以获得拥有任何这些属性的对象。

对象

说起对象,不得不提实例化、标志和封装。与构件的特性不同,对象的特性是:

(1)一个实例单元,具有唯一的标志。

(2)可能具有状态,此状态外部可见。

(3)封装了自己的状态和行为。

同样,对象的一系列属性随之而来。由于对象是一个实例化的单元,所以不能被局部初始化。由于对象有各自的状态,它必须有唯一的标志,以使它在整个生命周期内,无论状态如何变化,都能够被唯一地识别。

当对象被实例化的时候,需要一个构造方案来描述其状态空间、初始状态和新生对象的行为。该方案在对象存在之前就已经存在。显式存在的实例化方案称为类。也有隐式的实例化方案,即通过克隆一个已存在的对象来实现,即原型对象。

无论使用类的形式,还是用原型对象的形式来初始化一个对象,这个新生的对象都必须被设置一个初始状态。创建与初始化控制对象的代码可以是一个静态的过程——如果它是对象所从属类的一部分,就被称为构造函数。如果这个对象是专门用来创建与初始化对象的,则简称为工厂。对象中专门用来返回其他新创建的对象的方法常被称为工厂方法。

构件与对象

构件的行为显然可以通过对象来实现,因此构件通常包含了若干类或不可更改的原型对象。除此之外,构件还包括一系列对象,这些对象被用来获取默认的初始状态和其他的构件资源。

但构件并非一定要包含类元素,它甚至可以不包含类。实际上,构件可以拥有传统的过程体,甚至全局变量,它也可以通过函数语言,或者汇编语言,或者其他可用方法实现自身的全部特性。构件创建的对象——更确切地说是对这些对象的引用——可以与该构件分离开来,并对构件的客户可见。构件的客户通常是指其他的构件。除非构件的对象对客户可见,否则我们无从判断一个构件内部是否是“完全面向对象”的。

一个构件可以包含多个类元素,但是一个类元素只能属于一个构件。将一个类拆分进行部署通常没什么意义。另外,正如类之间可以通过继承关系等产生依赖一样,构件之间也可以存在互相依赖的关系——这种依赖很重要。一个类的父类并不一定与它的子类存在于同一个构件中。如果一个类的父类存在于外部的其他构件中,那么这两个类之间的继承关系便是跨构件的,这种关系将会导致相关联的构件间的导入关系。规约的继承是保证正确性的一项很关键的技术,因为共同的规约是构件间达成共识的基础。至于构件间对实现的继承是好是坏,仍然是众多学术流派争论的焦点。

模块

构件与模块的概念其实非常类似。模块的概念出现于20世纪70年代后期的模块化语言(Wirth, 1977; Mitchell等人,1979)。最广泛使用的模块化语言是Modula-2 (Wirth, 1982)和Ada。在Ada里面,模块被称做包,但其实两者是相同的。模块化方法成熟的标志是其对分离编译技术的支持,包括跨模块的正确的类型检查能力。

随着Eiffel语言的面世,类被认为是更好的模块(Meyer, 1988)。这似乎是正确的,因为我们最初的想法是每个模块实现一种抽象数据类型。毕竟,我们可以把一个类看成是一个抽象数据类型的实现,只不过它多了继承和多态的特性而已。然而,模块常常被用于把多个诸如抽象数据类型、类等实体打包到一个单元中。并且,模块没有实例化的概念,而类却有。

在其后出现的程序设计语言中——比如Modula-3,Component Pascal,和C#模块的概念(C#中的集合)与类的概念是区分对待的。在任何情况下,模块都可以包含多个类。在有些没有模块概念的语言(诸如Java语言)中,模块可以通过嵌套类来模拟实现。类之间的继承关系并不受模块界限的限制。另外值得一提的是,在Smalltalk系统中,经常会通过修改当前已存在的类来构造一个应用程序,人们已经开始尝试定义“模块系统”,这将使Smalltalk越过类而直接达到构件级水平,例如Fresco(Wills, 1991)。

模块本身就可以作为一个最简单的构件。即使不包含任何类元素的模块也可以实现构件的功能。传统的数值计算函数库就是一个很好的例子,这些库是功能性的,而不是面向对象的,但却可以打包成模块。然而,一个成熟的复杂的构件却并不是简单地仅用模块就可以实现的。模块没有持久不变的资源,有的只是那些被硬编码到程序中的常量。资源可以参数化一个构件。通过替换这些资源,就可以重新配置该构件而无需更改构件代码。例如,本地化设置可以通过资源配置实现。看起来资源配置好像为构件赋予了可变的状态值。但是我们知道,构件不能修改它们自身的资源,这些资源与编译后的代码一样只是构件的组成部分。追踪一个构件与它所派生的本地化了的构件之间的关联,在某种程度上,和追踪同一构件的不同的发布版本之间的关系相似。

某些情况下,模块并不适合作为构件,掌握这些情况是非常有用的。根据本书的定义,构件没有外部可见的状态,但是模块却可以显式地用全局变量来使其状态可见。并且,通过直接导入其他模块的接口,模块之间可以存在静态的代码依赖。而对于构件来说,虽然也允许存在对构件外部代码的静态依赖关系,但却并不提倡。这种静态依赖关系应被限定用于那些合约元素,包括类型和常量。使用间接而非直接的接口表示模块的依赖关系,把对实现代码的依赖关系限定于对象层次,就可以利用同一接口的不同实现来灵活地组装模块。

总的说来,模块化是构件技术产生的前提,但对于构件来说,传统的模块化的概念和标准是远远不够的。很多模块化的概念源自Parnas(1972),其中包括最大化内聚性与最小化耦合性这条基本原理。因此,模块化的思想并不新鲜。但遗憾的是,现今的大部分的软件仍然不是模块化的。比如,有不少的大型企业应用都是对一个单一的数据库进行操作,允许应用系统的任何一部分依赖于数据模型的任何部分。但构件技术则要求系统中各部分必须互相独立,或者存在可控的显式依赖关系。因此构件技术必将导致模块化的解决方案。这种软件工程效益充分说明对构件技术的投资是有价值的。

白盒抽象、黑盒抽象与重用

黑盒抽象与白盒抽象的区别主要在于接口“后面”的实现细节是否可见。在理想的黑盒抽象的情况下,客户对接口和规约之外的实现细节一无所知。而在白盒抽象中,在接口限制了用户行为并确保了封装性的情况下,客户仍然可以通过继承对构件的实现细节进行修改。由于在白盒方式中实现细节对外界是完全可见的,因此可以对实现细节进行研究,以加深对该接口抽象含义的理解。

揭示实现细节的可控部分。这是一个有争议的概念,因为部分可见的实现细节可以是规约的一部分。一个完整的实现只需要保证,能被客户看见的那部分实现细节与抽象的接口规约一致即可。这是将规约实现的标准方式。

黑盒重用指仅仅依赖接口和规约来实现。比如,在绝大多数系统中,应用程序接口(Application Programming Interface, API)完全与内部的具体实现无关。用这样的应用程序接口构造系统相当于黑盒重用这些接口的实现。

相反,白盒重用指依赖于对具体实现细节的理解,通过接口来使用软件部件。大部分类库和框架都会提供源代码,应用程序开发人员通过学习类的具体实现,就可以知道如何构造该类的子类。

在白盒重用中,被重用的软件不可以轻易地被另外的软件替换。如果贸然替换将有可能破坏正在重用的客户端,因为这些客户端依赖于那些在未来可能发生改变的实现细节。

根据上述特性可以得出以下的定义:“软件构件是一种组装单元,它具有规范的接口规约和显式的语境依赖。软件构件可以被独立地部署并由第三方任意地组装。”

这个定义最先是在1996年的面向对象程序设计欧洲会议上(European Conference on Object-Oriented Programming, ECOOP),由面向构件程序设计工作组(Szyperski和Pfister, 1997)提出。该定义涵盖了我们之前讨论的那些构件特性。它既包括了技术因素,例如独立性、合约接口、组装,也包括了市场因素,例如第三方和部署。就技术和市场两方面的因素融为一体而言,即使是超出软件范围来评价,构件也是独一无二的。

而从当前的角度看,上述定义仍然需要进一步澄清。一个可部署构件的合约内容远不只接口和依赖,它还要规定构件应该如何部署、一旦被部署(和启动)了应该如何被实例化、实例如何通过规定的接口工作等。事实上,各个接口的规约都应该被独立地看待,任何提供与使用该接口实现的构件之间都是相对独立的。比如,一个实现队列操作的构件通过一个接口获得物理存储空间,通过另外两个接口提供入队列和出队列的操作。在构件的合约中说明,通过入队列接口插入队列的元素,可以通过出队列接口中的操作取出来,这种关联关系,任何接口规约都不能单独提供。该合约同时也规定构件一旦被实例化,就必须在关联一个实现了物理存储空间接口的构件之后才能被使用。这种关联将受到底层构件模型的组装规则的影响。具体的部署和安装的细节由特定的构件平台提供。

$\color{green}{\text{接口}}$

接口是一个已命名的一组操作的集合。构件的客户(通常是其他构件)通过这些访问点来使用构件提供的服务。通常来说,构件在不同的访问点有多个不同的接口。每一个访问点会提供不同的服务,以迎合不同的客户需求。强调构件接口规范的合约性非常重要,因为构件和它的客户是在互不知情的情况下分别独立开发的,是合约提供了保证两者成功交互的公共中间层。

成功的合约接口需要遵循哪些非技术因素?首先,必须时刻关注经济效益。一个构件可以有多个接口,每一个接口提供一种服务。有一些服务会格外受客户欢迎,但是如果所有服务都不受欢迎,那么服务的组合也不会受欢迎,这个构件就没有市场价值了。这样的话,就没有必要在非构件的实现方案的构件化上进行投资了。

其次,应当避免不当的市场分化,因为这威胁到构件的生存。所以,尽量不要重复引入功能相近的接口。在市场经济中,这通常是主要生产商在市场早期努力推行标准化的结果,或者是经过残酷的市场竞争优胜劣汰的结果。但是,前者可能会由于笨拙官僚的“委员会设计”问题而不能达到最优;而对于后者,市场竞争的非技术本质也可能导致结果不是最优的。

最后,为了使一个接口的规范和实现该接口的构件得到广泛应用,需要有一个公共传媒来向大众进行宣传和推广。要做到这一点,至少需要几种能被广泛认可的保证命名唯一性的命名方案。 $\color{red}{\text{接口标准化}}$ 的一个非常有意思的变种,是对 $\color{green}{\text{消息的格式}}$ 、 $\color{green}{\text{模式}}$ 和 $\color{green}{\text{协议}}$ 的标准化。它不是要将接口格式化为参数化操作的集合,而是关注输入输出的消息的标准化,它强调当机器在网络中互连时,标准的消息模式、格式、协议的重要性。这也是因特网(IP、UDP、TCP、SNMP等)和Web(HTTP、HTML等)标准的主要做法。为了获得更广泛的语义,有必要在一个单一通用的消息格式语境中标准化消息模式。这就是XML的思想。XML提供了一种统一的数据格式。

显式语境依赖

在上文的构件定义中,构件除了要说明所提供的接口外,还要说明其需求。也就是说为了使构件正常地工作,必须说明其对部署环境的具体要求。这些要求被称为语境依赖,指的是构件组装和部署的语境,包括了定义组装规则的构件模型和定义构件部署、安装和激活规则的构件平台。如果只存在一种软件构件体系的话,那么只需要列举该构件所需的所有其他构件提供的接口,这样就足够可以说明全部的语境依赖。例如,一个合并邮件的构件会声明它需要一个文件系统的接口。但是,今天的大多数构件即使连这样的需要的接口也通常不进行声明。而构件提供的接口更受关注。

事实上,目前有几种构件体系同时存在,它们相互竞争,彼此冲突。例如,现在就有OMG的CORBA,Sun的Java,以及微软的COM和CLR(Common Language Runtime)等体系。并且,因为要支持不同的计算和网络平台,构件体系本身并不是单一的。这种状况还没有很快改善的迹象。而另外一种观点则认为,所有这些构件体系最终都只能归结为两类——CORBA+Java体系和微软体系(包括COM+和.NET/CLR)。但即使构件体系被刻意减少到不能再少的区区两种,在具体实现层次上还是存在着千差万别的。

构件的规模

显然,构件只有在提供了“恰当”的接口集,以及对语境依赖没有严格限制的情况下,该构件可以在所有的构件体系中运行,并且其依赖的接口不会超出那些构件体系所能提供的范围时最好用。然而,只有极少的构件拥有这么弱的环境依赖性。技术上来说,一个构件可能和它所需要的所有软件捆绑起来被提供,但这显然违背了使用构件的初衷。注意,环境需求往往取决于构件运行的目标机器。如果是虚拟机,例如Java虚拟机,这就显然是该构件体系规范的内容之一。如果是本地代码平台,仍然有类似于Apple的将多个二进制文件打包成一个文件的Fat Binaries这样的机制,可以使构件在“所有地方”运行。

构件设计者通常不会构造自给自足的构件,将所需的所有东西都打包进来,而是采取一种“最大化重用”的策略。为了避免在构件中重复实现那些次要的服务,设计师通常会只实现该构件的核心功能,然后重用其他所有的一切。面向对象的设计有向这种极端发展的趋势,许多面向对象的方法论者都大力提倡这种最大化重用的思想。

虽然最大化重用的思想有很多为人称道的优点,但是它也有一个潜在的缺点——语境依赖的爆炸性增长。如果构件在发布后其设计一直冻结不变,同时所有的部署环境也都一样,那么这个问题就不会出现。然而,构件会不断地演化,不同的部署环境会提供不同的配置,多种版本会同时存在,在这样的情况下大量的语境依赖只会使构件成为众矢之的。语境依赖越多,能满足构件环境需求的客户构件就越少。总之,最大化重用降低了可用性。

构件设计者需要为以上两者找到一个平衡点。当要描述构件的基本接口的时候,设计者们就需要做出抉择。增加语境依赖通常会使构件因重用而简洁,但却会降低可用性。此外,还必须考虑环境的演化会使构件更加脆弱,例如引入新版本带来的变化。增加构件的自给性可以减少语境依赖,增加可用性,并且使构件更健壮,但却会使构件规模过大。

标准化与规范化

通过提高接口与构件体系的标准化和规范化程度,可以使上文优化问题中的最优点偏向于简洁性一侧。一个东西越稳定,越容易被广泛接受,其成为某个构件的特殊需求的危险就越小。如果语境依赖能够被广泛支持,就不是什么缺点。比如仅仅在50年前,要求客户必须拥有电话才能谈妥生意是极不明智的。而现在,在世界上的许多地方,拥有电话已经成为必要条件。

通用市场与专业市场

如果要制定一种覆盖所有领域、有广泛市场的标准,就有必要区分面向通用市场与面向专业市场的两种标准。通用市场覆盖了多数甚至全部不同的市场领域;它对所有或绝大多数的客户和生产商都有影响。专业市场往往只限于某个特定的领域,相对来说影响比较小。例如,因特网和万维网的标准都属于通用市场标准。与之相反,放射医学领域的标准就只影响一个比较窄的专业市场,却同样会占有相当大的市场份额。

通用市场的标准化是非常困难的。如果有一项服务几乎和每个人都相关,那么它就得满足所有人的需求。想像一下那些通用程序设计语言标准化委员会,他们为顾全各方面的利益而疲于奔命。与此同时,成功的标准只有在通用市场中才能形成最广泛的影响,网络标准就是其中最好的例子。

令人吃惊的是,专业市场的标准化与通用市场同样艰辛,虽然原因各不相同。由于专业市场中涉及的人相对较少,所以比较容易形成一种折中的方案。然而,如果某个专业领域正在考虑标准化,那么为了培育市场,该领域就不能够太窄。由于所涉及的人较少,市场经济的机制就不容易很好地发挥作用,也就不太可能在短期内找到理想的、成本效益好的解决方案。

标准的构件体系与规范化

在基本构件体系和那些最重要的接口合约形成标准,并且在这些标准被相关的工业界支持的情况下,构件技术最为成功。然而,要发挥标准化的作用,就必须使与之竞争的其他标准的数目尽量很小。如果某个标准背后有一个强大的国际标准化组织认可,有一个非常有实力的企业推动,有众多有影响力的公司或组织联合支持,那么一切自然不成问题。然而,通常却是几个标准在相互竞争。如果因专业市场各自为政,导致某标准在不同领域重复建设,而该标准又可能适合其他领域,就可能出现戏剧性的结果:很多原先互不知情的标准竞争者在一夜之间一起出现。例如,放射医学和射电天文学就可以共享多种图像处理标准。

如果相互竞争的标准过多,而其相应的市场份额过小,就可能引发危机,这个问题可以通过规范化的手段来解决。公布共同的设计“模式”,并对其进行编目,原来互不知情的各方标准化实体就有可能在各自的目标领域发现共同点。当然,寻找和利用共同点的努力是否值得,即成本效益是否理想是个规模效益的问题。

构件的布线标准

“布线”用于连接电子构件。无论天然气、水或排污系统,对于所有要连接的部件来说,管道工程本质上是相同的。对于可连接的构件来说,这一级的标准是重要的。但是,需要注意的是不要高估了“布线”标准的重要性。比如说,庞大的世界范围内的电话系统之间的互联互通就是一个例子。

布线标准从何而来

由于过程的交互为进程边界所限,所以操作系统支持多种多样的进程间通信(Internet Process Connection, IPC)机制,典型的例子有文件、套接字(socket)及共享内存。除了BSD-UNIX套接字外,这些机制都不能跨平台移植。

IPC机制的一个共同的优势是:它们可以很容易地被扩展到网络甚至是因特网上。这是传统进程模型的直接结果。在这个传统进程模型中,每个进程产生了一个幻象,就好像一个共享的物理主机上的每个进程都拥有单独的虚拟机。

RPC的设想是在本地被调用者和远程调用者两端都使用指代(stub)。调用者使用严格的本地调用约定,就像调用了一个本地被调用者,实际上,却调用了一个本地指代来编排(串行化)参数,并把它们发送到远端。在远端,另一个指代接收参数,并还原(反串行化)参数,然后调用真正的被调用者。和调用者一样,被调用者的过程本身也要遵循本地调用约定,并且不知道自己被远程调用了。编排和还原过程负责转化数据值,将它们从本地表示转化为网络格式,然后再转化为远端表示。通过这种方法,格式的差异等被跨越了。

分布式计算环境(DCE)是OSF (Open Software Foundation, Open Group的一部分)的一个标准,它是在跨越异构的平台上实现RPC机制的最重要的服务。在另一个极端,轻量RPC变化能被用来处理单机上的IPC问题。例如,Windows支持跨进程的轻量RPC;DCOM出现后,可以支持不同机器间的完全RPC。DCE也通过对每个服务附加主版本号来支持版本控制。客户可以指定他们想要版本的服务。

潜在的透明性既是RPC的优点,同时也是其负担。因为它隐藏了本地调用、进程间调用及机器间调用的很重要的代价上的差异。在大多数当前的体系结构上,进程间调用比本地调用慢10~1000倍,而机器间调用比进程间调用慢10~10000倍。

RPC
既然有 HTTP 请求,为什么还要用 RPC 调用?:RPC的接口规范基于代码。http的接口规范基于文档。
RPC实现原理之高并发场景下的技术运用
RPC是在什么场景使用?

接口定义语言(Interface Definition Language, IDL),保证了不同环境下过程调用语义的一致性。对每一个可以被远程调用的过程,IDL指定了参数的数目、传递模式和类型,以及可能的返回值的类型。为了确保跨机器边界的通信正常工作,所有的IDL都必须固定基本类型的范围,例如指定整数是32位的二进制的补码值(二进制的补码表示是一种以二进制形式表示负数的数学方案)。

过程调用及它们的二进制调用约定提供了一个良好证明的“布线”标准。但是它们还不能直接支持对象所需要的远程方法调用。如果和动态链接库(Dynamic Link Library, DLL)结合起来,远程过程调用就会在为构件“布线”形成一个有用的基础的过程中更进一步。服务可以通过名字(DLL的名字)来定位,并被动态地绑定(不仅在编译时刻),而且服务可以是远程的。今天,Web服务似乎已成为事实上的构件布线标准。

从过程到对象

使对象调用与过程调用区别开来的首要因素在于它们很靠后的、数据驱动的调用代码选择。一个方法调用,除非是优化过的,否则总会去检查接收消息的对象的类,并从该类提供的方法中选择方法实现。而且,一个方法总是将目标对象的引用作为另一个参数向外提供,该引用是消息发往的地址。面向对象编程的大多数优点来自方法调用的属性。

有趣的是,当前的对象调用并不遵循标准平台调用约定。原因很简单,就是由于当前的操作系统和它们的库有过程化的接口,因而对操作系统提供商来说从来就没有必要定义方法调用约定。结果,使用不同的编译器编译出来的代码就无法互操作,甚至使用同一种语言实现的代码也不行。为了通用,一个面向对象的库必须以源代码的形式发布。这也是为什么可执行类库(而不是源代码)在这种面向对象的情况下远不及在过程化的情况下流行的原因。

在实现了过程调用的机器上实现方法调用是可能的。比如IBM的系统对象模型(System Object Model, SOM)就是这么做的。在SOM中,所有的语言绑定只是简单地调用SOM库过程,然后在SOM运行时刻动态地选择要调用的方法。CORBA的ORB(Object Request Broker)是另一个例子——最新版本的SOM实际上就是基于CORBA的。Microsoft的COM也非常接近于仅使用过程调用约定,虽然它依赖包含函数指针的过程变量表(也称分配表)。但这不是问题,因为过程变量很久以来已经是调用约定的一部分。

另一个可能的方法是为方法调用定义一个带有内建支持的新的虚拟机层。这就是Java虚拟机和.NET的运行时刻公共语言采用的方法。但是,和库的支持及系统范围的调用约定不同,虚拟机可能阻止或干扰超越它的边界的互操作。因此,JVM和CLR都为跨越虚拟机边界的互操作提供特殊的支持。

深层次问题

如果在执行层上过程调用约定几乎是能胜任的,那为什么还要有这么多不同的竞争的提议呢?原因在于,为了实现互操作,还有其他的重要方面需要被考虑和标准化。需要回答的问题包括“接口如何指定”、“当离开它们的本地进程后对象引用如何处理”、“服务如何被定位和提供”及“构件演化如何处理”。

接口和对象引用规范

什么是接口?所有当前的方法一律将接口定义为一个已命名的操作的集合,每个操作带有一个已定义的特征标记(signatrue)和可能的返回值的类型。操作的特征标记定义了该操作的参数的数目、类型及传递模式。接口和什么相连接?对于这个问题,每种方法的处理是不同的。那些基于传统的对象模式的方法在接口和对象之间定义一个一对一的关系(CORBA 2.0, SOM)。对象在接口后面提供状态和实现。其他的方法将多个接口和一个单独的对象联系起来(Java, CLR),或者把多个接口和一个构件对象的多个部分对象联系起来(CORBA 3.0的COM, CCM)。明显地,一旦出现一个接口后面有多个对象的情况,就有身份标志的问题产生并且需要处理(为了这个原因,COM和CCM提供了一个特殊的接口)。

如何指定接口?所有传统的处理方式都遵循DCE(Data Communications Equipment),并且使用IDL。遗憾的是,真正使用的不是单个的IDL,而是存在着的几个竞争的提议,特别是OMGIDL和COMIDL这两个最强大的竞争者。Java和CLR没有IDL,这里,相关信息作为元数据被保留并可以映射到任何被支持的语言。程序员可以使用他们熟悉的语言来看待接口和其他类型定义,而不需要去学习他们熟悉的语言之外的一种IDL。所谓的Java IDL,实际上是一种结合了一个IDL到Java编译器的Java可调用的CORBA ORB。通过一个从Java类型到OMG IDL(反之亦可)的映射来支持Java。在OMG IDL和COM IDL之间也已经定义了一个相似的双向映射。目前OMG IDL和CLR类型之间或者和更多的特定于C#的类型之间还没有相似的映射。

什么是对象引用?当它们作为一个远程方法调用的参数被传递时是如何被处理的?每种方法的做法是不同的。但所有的方法都有一些机制,用于把本地的有意义的引用映射到包含跨进程、机器及网络边界的引用。

接口关系和多态性

所有的方法都规定了多态性。在所有的情况下,某个具有一个已知接口的实体可以是多个不同的、可能实现中的一个。同时,在所有的情况下,一个实现所能提供的方法比接口指定的要多。

在细节上,所有的做法都是不同的。CORBA 2.0遵循一个传统的对象模型。一个对象有一个单独的接口,但这个接口可能是由其他的接口使用接口多继承组合而成的。实际提供的接口可能是所期望接口的子类型,其额外附加的能力可以被动态地发现。CORBA 3.0中的CCM支持多接口继承及在它们间动态的导航。COM也明确地支持除了被共同支持的已经提供的接口之外的必需的接口。COM拥有不变的接口,也就是说一旦发布则不可扩展或修改。单接口继承被支持用来从已发布的接口派生出新的接口。但是,一个COM对象可以拥有多个接口。对一个特定的对象来说,它提供的接口的集合可能会随着时间的流逝而变化,并且可以被动态地发现。通过约定,COM支持必需的接口。一个Java对象也可以实现多个接口,但是这更接近于多接口继承而不是COM那样的完全分离的接口,一个对象所实现的接口的集合由这个对象的类静态地确定。同样的情况也适用于CLR对象。Java接口的多继承的传统在试图支持有冲突的方法名的接口时会导致问题的出现——Java对象不能支持多个这样的接口。COM和CLR都维护不同接口上的方法在实现层次上的分离,而不关心方法的名字。CLR模型在支持多接口继承方面也超出了COM,像Java或CORBA。和COM一样,Java和CLR仅通过约定的方式采取了支持必需的接口的做法。

命名和定位服务

接口是如何命名的?它们是如何相互关联的?没有两种做法对这两个问题的处理是完全一致的。COM采用了DCE的UUID(Universally Unique Identifier)的做法,在COM中被称为全局唯一标志符(Globals Unique Identifiers, GUID)。GUID用来唯一地命名多样性的实体,包括接口(Image Impoet Descriptor, IID)、接口的组(称为分类(Category ID, CATID))以及类(Class ID, CLSID)。OMG CORBA最初是把唯一命名留下用来单独实现的,依赖语言的绑定来维护程序的可移植性。在CORBA 2.0中,引入了全局唯一标志符。这些既可以是DCE UUID,也可以是类似于常见的用于万维网的统一资源定位符(URL)的字符串。Java完全依赖由内嵌的命名包来建立的唯一的名字路径。CLR提供类似的有资格的名字来建立一个可读的命名方案,但最后把所有的名字都放入那些所谓的强组装名字(strong names of assemblies)。其实有很大的可能性,私有/公有密钥对中公有的那一半是独一无二的。这就是说,尽管标志符使用唯一的标志符来支持个别的名字,但CLR可以使用一个独一无二的标志符来支持整个名字家族,只要这样的名字家族是在一个单独的组装中(一个CLR软件构件)被一起发布的。通过给定一个名字,所有的服务都提供一些注册表或者库的分类来帮助定位相应的服务。在这个类似目录的功能之上,所有的方法都还提供某种程度上的关于可用服务的一些元信息。所有方法都支持的最小的功能是,对被提供的接口类型的运行时刻的测试、接口的运行时刻反射及新实例的动态生成。

复合文档

软件构件的第一个实用方法是复合文档模型。复合文档是一个模型,对那些构成,即用户来说,在模型中构件及其合成具有直观的意义。Xerox Star系统是第一个基于Xerox Palo Alto研究中心的研究结果的,但是没有能够获得足够的市场和观念份额。第一个突破是苹果公司的Hypercard,因为它有简单直观的合成及使用模型,但是创建新的构件却是一件困难的事情。Microsoft的Visual Basic也遵循了它,对Visual Basic控件有一个合理的编程模型。有了Microsoft的OLE和苹果公司的OpenDoc技术后,一般的文档都可以遵循它了。后来,嵌入对象比如Java applets和ActiveX控件的网页的出现,增加了一个新的维度。

在OLE中,复合文档的概念更前进了一步。首先,任意的容器可以被允许。除Visual Basic表单外,Word文本、Excel电子表格、PowerPoint幻灯片等,都变成了OLE(Object Linking and Embedding)容器。而且,“控件”的概念被推广到任意的文档服务器中去。然而最大的变化是构件可以同时是文档容器和服务器。结果,Word文本能被用来注释一张PowerPoint幻灯片,而这张幻灯片也能被嵌入到另一Word文本里去。

复合文档还可以是一个把对象嵌入到了HTML页面里的网页。浏览器为所有的Web页面提供一个统一的文档模型。嵌入对象,比如Java applets,能根据需要加入细节。但是,现在有了强大的服务器端Web编程模型,例如Sun的JSP(Java Server Page)和Microsoft的ASP(Active Server Pages)及目前的ASP.NET。当合成一个基于后端数据和用户输入相结合的复合文档的时候,现代Web页面远远超过OLE技术的地方,正是这种服务器端模型。

XML

尽管1998年才出现,但XML(扩展标记语言;W3C, 2000b)在此前大量尝试都失败的地方获得了成功。一部分原因是由于XML的一些有趣的属性;一部分是由于合适的时机。XML的到来与一个数量大量增长着的领域相关,特别是电子商务的领域需要用XML标准化。

XML对于表示任何(半)结构化的数据十分有用,新的XML的应用就应运而生了。除了消息、Web页和传统文档之外,现在普遍用XML来配置数据,即使这些数据从未被其他任何应用(而不是为之定义方案的应用)处理过或者阅读过,使用XML仍然是有用的。浏览器比如Internet Explorer,直接支持显示和查找XML文档。还有很多的工具为基于XML的数据提供其他普遍支持的形式,这些工具包括编辑器、方案检查器和方案驱动的翻译器。没有异常情况的话,XML在独立起源和操作的应用间作为一门公共语言来使用会获得最大的好处。在某种意义上,XML成了从协议和“布线”格式层到持久数据表示层的“布线”标准的概念。

构件框架

体系结构

系统的体系结构是任何大规模软件技术的关键基础,在基于构件的系统中起着至关重要的作用。只有当整体的体系结构良好地定义和维护,构件及系统的升级和维护才会有坚实的基础。构件体系结构的核心包括:构件和外部环境的交互;构件的角色;标准化工具的界面;对最终用户和部署人员的用户界面等。

体系结构的角色

体系结构是关于一个系统的整体视图,一个体系结构从总体上定义了总体的不变性,即那些根据这个特定的体系结构建立起来的所有系统的共同属性。体系结构把核心资源分类,以支持在资源竞争下的独立性。操作系统就是一个很好的例子,通过定义独立的进程之间如何竞争资源,操作系统部分地定义了运行于其上的系统所采用的体系结构。

体系结构为所有涉及的机制规定了恰当的框架,限制自由度,以控制变化性并支持协作。体系结构包括了所有支持独立使用机制进行互操作的策略决策。策略决策包括构件的角色。

体系结构需要基于对整体功能、性能、可靠性和安全性的主要考虑过细的决策可以放在一边,但关于所期望层次功能和性能的指导是必须的。例如,体系结构可能确切地规定一些细节来保证性能、可靠性或者安全性。在安全关键的应用中,有强调这些所谓非功能需求的传统。在任何体系结构中把这4个方面都作为一个整体的高优先级问题仍然是一个重要的目标。

概念化

在概念层次上,划分层次、标志构件、分离关注点的作用是显而易见的。但在一个具体的体系结构中,它们是否还存在?更具有争论性的是,超越对象的粒度是否真有必要?有趣的是,有时认为对象最主要的优势是对象和对象间的关系在需求、分析、设计和实现等阶段是一致的(Goldberg和Rubin, 1995)。这种说法的成立需要两个前提,一是如果在所有的上述过程中都只有对象起主要作用;二是系统中所有超越对象的事物都可以被隔离。这两个前提也是所谓的“纯”面向对象方法的主要动机。

显然,并不是所有的事物都是对象。然而,任何需要一组对象进行交互的系统都可以通过指定一个代表对象来抽象这个交互对象组。此时,区分“has a”(或者“contain a”)联系和“use a”关系就变得很必要了。这个代表对象“包含”(“has a”)对象组,而组中的对象之间也可以通过代表对象的协调而相互使用(use)。以图的形式建模对象之间的关系时,对象是节点,联系是这些节点间的有向边。“has a”和“use a”分别是图际边和图内边。让我们考虑在时间和空间语境中支持对象转换的外部服务,例如,存储复合文档。在典型的外部行为中,图内边需要追溯下去;图际边不需要追溯,但需要抽象地保持为“连接”,连接象征性地代表了有向边的目标节点。

构件系统架构特性

● 构件系统体系结构由一组平台决策、一组构件框架和构件框架之间的互操作设计组成。

平台是允许在其上安装构件和构件框架的一个基础设施,支持构件和构件框架的实例化和激活。平台可以是实际平台,也可以是虚拟平台。实际平台提供了直接的物理支持——也就是在硬件上实现了它们的服务。虚拟平台(也可以称做平台抽象或者平台外壳)在其他平台之上仿真了一个平台,以支持灵活的成本权衡能力。

● 构件框架是一种专用的体系结构(通常围绕一些关键的机制),同时,也是一组固定地作用于构件层次机制的策略。

构件框架常常实现一些协议以连接构件,并强制实施一些由框架决定的策略。管理如何使用框架自身所用机制的策略并不确定。实际上,它们可以留给更高一层的体系结构来确定。

● 概念框架的互操作设计包括系统体系结构连接的所有框架间的互操作的规则。

这样的设计是第二等的构件框架,构件框架可以看成是它的内插构件。到现在我们可以确信第二层次是必要的——包含所有内容的单个构件框架是不切实际的。现在还不清楚第三层或者更高的层次是否必要,但此处暗示的元体系结构模型是可扩展的,允许增长。

●  $\color{red}{\text{构件}}$ 是一组通常需要同时部署的 $\color{green}{\text{原子构件}}$ 。构件和原子构件之间的区别在于,大多数 $\color{red}{\text{原子构件}}$ 永远都不会被单独部署,尽管它们 $\color{green}{\text{可以被单独部署}}$ 。

相反,大多数原子构件都属于一个构件家族,一次部署往往涉及整个家族。

● 一个原子构件是一个模块和一组资源。

原子构件是部署、版本控制和替换的基本单位。原子构件通常成组地部署,但是它也能够被单独部署。一个模块是不带单独资源的原子构件(在这个严格定义下,Java包不是模块——在Java中部署的原子单元是类文件。一个单独的包被编译成多个单独的类文件——每个公共类都有一个)。

● 模块是一组类和可能的非面向对象的结构体,比如过程或者函数。

显然,一个模块可能静态地需要另一个模块的存在才能起作用。因此,一个模块只有在其依赖的所有模块都已经可用后才能部署。这个依赖图必须是无循环的,否则一组循环依赖关系的模块总是需要同时部署,这就破坏了模块定义的性质。

● 资源是一个类型化的项的固定集合。

资源这个概念可以包含代码资源,进而包含模块。问题在于除了编译器编译一个模块或包生成的资源外,还可能存在其他的资源。在“纯对象”的方法中,资源是外部化的不可改变的对象——不可改变是因为构件没有持久化的标志,而且复制不能被区分。

分层的构件体系结构

层的概念和层次分解在构件系统中十分有用。构件系统的每一个部分,包括构件本身,都可以被分层,因为在一个更大的体系结构中,构件可以被定位到特定层次。为了控制更大型的构件系统的复杂性,体系结构自身也需要分层。

如前所述的构件系统体系结构具有一组开放的构件框架。这组构件框架形成了第二水平层次,而每个构件框架都定义了第一水平层次的体系结构。在这里,区分水平分层和传统的垂直分层之间的本质区别非常重要。传统的垂直分层,自底向上地,抽象程度渐增,与应用相关的性质逐渐提高。在一个良好的垂直分层系统中,各个层次都应该考虑相应的性能和资源。相反,水平分层是性能和资源相关性递减而结构相关性渐增的。不同的水平层次关注不同的集成性,但都与同一个应用相关。图9-1描述了在一个三水平分层多垂直分层的体系结构中垂直分层和水平分层的相互影响。如同描绘的那样,高水平分层提供了共享的低垂直分层以集成低水平分层。水平分层被描绘成相邻的,而垂直分层则是一个叠于另一个的上面。

图9-1 三水平分层多垂直分层的体系结构——构件、构件框架和构件系统找不到图片(Image not found)

图9-2展示了构件实例之间如何相互通信。它们可以直接通信(例如,通过使用COM可连接对象、COM消息服务的消息、CORBA的事件或者JavaBean的事件),也可以通过构件框架做中介来间接通信,这时构件框架可以规范构件间的交互。当构件框架实例交互的时候同样的选择又会发生——这次的中介者是第三水平层次的运行实例。在图9-2中,CI代表构件实例,CFI(Component framework instance)代表构件框架实例,CFFI(Component Framework of Framework Instance)代表构件系统(或者构件框架的框架)实例。

在单体软件处于主导地位的世界里,甚至第一水平分层的体系结构都是不常见的。值得一提的是,对象和类框架并没有形成最底层的水平分层。水平分层的结构是从可部署的实体——构件开始的。传统的类框架只能形成单独的构件,独立于水平分层体系结构的布局。对象和类框架可以存在于构件内部。这些对象和类框架可以形成自己的层次,这取决于构件的复杂性,比如在OLE中的MFC。但是,当编译构件的时候所有类框架的结构都会被展平。跟构件框架不一样的是,类框架和它的实例间的界限是很模糊的,这是因为这个框架在运行时刻并不是实体,而在编译时刻,实例并不存在。这种二重性也可解释我们对术语“类”和“对象”常见的混淆。

图9-2 多水平分层体系结构中的自由与间接的交互找不到图片(Image not found)

轻量级体系结构把注意力集中到一个问题,而不覆盖所有的问题,以有效支持轻量级构件的创建。这种构件的创建在一些限制性假设的前提下很容易想象出来。如果轻量级构件的指导性体系结构在限定其他决策的同时支持较好的易扩展性,那么它的商业价值就非常大。

中间件是一个软件集合的名字,这些软件位于操作系统和高层次分布式编程平台之间。中间件有时被分为面向消息的中间件(Microsoft Operations Manager, MOM)和面向对象的中间件(Object Oriented Method, OOM)。然而,现有的大多数中间件都是这两种类型的混合体。当然,现在也有一种趋势是由传统的操作系统直接支持。操作系统总是包含了对通信协议的支持。Web服务的推进和程序世界从以程序为中心到以协议为中心的转变,导致两种中间件的价值观:支持合适的协议或者提供简化本地服务构造的结构。

独立的中间件产品,如消息队列系统、事务处理监控器或者集线器,已经慢慢地消失了。取而代之的是结合了中间件功能和某个特定构件框架的特殊的服务器。应用服务器结合了应用管理、数据事务、负载平衡和其他的功能。集成服务器结合了协议转换、数据变换、路由和其他功能。工作流和复杂交互服务器结合了事件路由、决策和其他功能。

应用服务器市场有很多种不同的产品,包括IBM的MQ系列工作流系统和Microsoft的BizTalk服务器。集成服务器市场可能是最分散的,有各种提供商提供的各种类型的产品,包括CrossWorlds,IBM(WebSphere B2B Integrator),Microsoft(BizTalk server),Oracle(XML lntegration server),Tibco(ActiveEnterprise),WebMethods(Enterprise)和WRQ(Verastream)等。

构件与生成式编程

生成式编程致力于通过转换的方法来构造软件。这种转换对软件工程师来说并不陌生。编译器把源代码转换成目标代码,JIT编译器将中间代码变换成机器代码。然而,生成式编程试图超越传统的转换方法中转换器固定不变的弊端。其思路是允许程序员定义新的转换器。Czarnecki和Eisenecker(2000)详尽探讨了一个方法,它使用C++模版来定义变换。他们同样也讨论了很多其他的方法,包括诸如GenVoca家族(Batory和O\Malley,1992)中的特殊的生成技术。而Biggerstaff则更广泛地讨论了生成式方法的动机(1998)。在可部署构件的世界中,生成式方法在两个领域里面起着重要作用。它们可以用来生产单独的构件,也可以用来增强由构件组装的系统。如果用于生产单独的构件,生成式方法就限定在单个构件中。当目标是生产规模较大的构件或者是生产潜在的数目较大的相关构件时,这个方法显得特别有用。仔细挑选可以被边界条件参数化的技术是很重要的,这些边界条件是对生成构件的需求。特别地,必须要精确控制实际的构件边界,包括提供接口和需求接口。此外,必须能精确控制同其他构件间的静态依赖。

语境相关组合构件框架

构件框架使构件化软件发展成为最重要的一步。当前大多数研究的重点都放在单个构件的创建和基本构件间的绑定支持。在这种条件下,独立开发的构件几乎不可能进行有效的协作。因而,构件的独立部署和集成将无法实现。

构件框架是一个软件实体,该实体支持符合某种标准的构件,允许这些构件的实例“插入”构件框架。构件框架为构件实例创建了环境条件、规定了对象实例间的交互。构件框架能单独存在并为某些构件创建其生存空间,构件框架也能与其他构件和构件框架协作。因而我们可以很自然地把构件框架自身建模为构件。这样,我们可以在构件框架的基础上建立更高层次的构件框架,该构件框架规定了其底层构件框架间的交互。

构件框架关键性的贡献在于体系结构准则的部分强制要求。在构件框架的控制下,通过强制构件实例执行某种任务,构件框架能够强制某些策略执行。我们来看一个具体的例子,一个构件框架可以强制规定事件多播时的某种顺序,这样就排除了一类由于误操作或竞争而导致的难以捉摸的错误。

第一个对语境相关组合提供商业支持的也许是COM的“套间”模型。事实上,其下一代,MTS(Microsoft Translation Server)语境,能看做是当前所有语境相关组合方法(EJB容器、COM+语境、CCM容器和CLR语境)的源头。顺便提一句,必须注意EJB(Enterprise JavaBean)和CCM(Component Category Model)容器紧密地对应于MTS,COM+和CLR语境,而不是对应于OLE和ActiveX容器。一个OLE和ActiveX容器并不截取所有内含的控件的输入或输出调用。

COM+语境

COM+源于COM“套间”和MTS语境。COM“套间”使用线程模型来分离对象;MTS语境通过事务域分离对象。COM+统一了这两个概念,同时也加入了大量的新的语境属性。无论哪种情况,用于驱动语境运行时语境的构造和将对象放置在合适的语境中的都是公开声明的属性。在COM“套间”的情况下,属性声明采用每一个COM类都有一个注册表项的形式。注册的条目要求类的实例必须仅仅被放置在单线程“套间”中,或者仅仅被放置在多线程的“套间”中。(COM+增加了可租赁线程“套间”的概念,在这种类型的“套间”中一次只允许一个线程入住,但是多个线程能顺序地入住该“套间”。)

微软事务服务器(MTS)引入了事务语境的概念。在MTS中运行的COM类的事务声明属性规定,该类必须或者位于非事务语境,或者位于新的事务语境,或者位于新的或是已有的事务语境,或者不做任何要求。通过这些声明,MTS和分布式事务协调器(Distributed Transaction Coordinator, DTC)共同创建了一个合适的事务域。相同事务域中的对象共享一个单独的逻辑线程和一个单独的共享事务资源集合(比如数据库连接和锁)。一旦线程从事务域中返回,事务要么提交要么终止,该事务域被销毁,该域所持有的资源被释放。

对于MTS的事务域,COM+增加了临时从一个事务域中返回、保持当前正在进行的事务的状态和在下一次调用时继续执行事务的功能。该模型允许客户端通过多个调用/返回接口进入一个事务服务的对话。(COM+也极大地扩展了语境属性、域和声明配置等概念。通过与以前的微软消息队列(Microsoft Message Queue, MSMQ))。服务集成以及加入几个其他的服务,COM+具有了大量的声明属性。COM+还引入了队列构件,这种类型的构件通过接收消息而实例化。

在COM+中,如果两个构件共享一组兼容的语境属性集,则它们可以被看做是处在同一域中。比如,如果两个对象共享相同的事务ID,则它们处于相同的事务域中。MTS中的域能够扩展进程和机器的边界。而MTS中的语境自身对进程进行了划分,而不是扩展了进程边界。相同语境中的对象具有相同的语境属性。域是同一属性的分组,而语境则基于属性集。图9-3显示了三个语境,两个属于相同的事务域(c2和c3),两个属于相同的负载平衡域(cl和c2)。这两个域都含有相同的语境c2。图中还显示了三个语境中的4个对象。对象u,v和w共享相同的负载平衡资源(资源ID为7),对象w和x共享相同的事务(事务ID为42)。

图9-3 域和语境找不到图片(Image not found)

跨越语境边界的调用被拦截,然后根据语境属性或者被预处理,或者被置后处理,或者被拒绝。

EJB容器

EJB为EJB实例提供了容器。虽然这些容器在MTS语境的基础上被模式化,但它们仍然有自己——也就是说,该类的实例需要直接调用事务API来开始、提交和结束一个事务。如此明确的控制使得这些类组装更加困难,但是当添加合法的事务代码时这种控制开辟了一条更加容易的道路。明确的控制在COM+中是可能做到的,但是不如EJB直接(本质上,外部控制需要独立存在于COM+的事务域之外,并且和微软的分布式事务控制器DTC直接发生交互)。

此外,EJB容器通过支持会话Bean和实体Bean支持持久对象。会话Bean的行为类似于MTS语境中的COM实例——旦事务结束(异常中断或是正常提交)其状态将丢失。实体Bean在事务正常提交时就保存至事务永久存储器。也就是说,Bean能够作为永久对象保存,除了可显式编写语句——这是MTS或COM+的唯一选择。

从版本2.0开始,EJB提供了改进的由容器管理的持久性和关系,提高了EJB实体Bean从一个应用服务器的容器到另外一个应用服务器容器的可移植性。EJB 2.0也增加了消息驱动的Bean类型,这是一个完全由数据驱动的EJB构件类型。与无状态会话Bean一样,消息驱动的Bean也是在消息到达时实例化,消息处理完毕后被销毁。这类似于COM+中的队列构件。消息驱动激活的概念可以追溯到IBM信息管理系统(Information Management System, IMS)的事务管理功能,该系统激活和停止IMS程序以处理队列消息。

CCM容器

CORBA构件模型(CCM),CORBA 3.0规范的语境中被引入。正如EJB建立在MTS的概念上一样,CCM建立在EJB的概念上。CCM容器可以定义为EJB容器的超集(EJB规范和CCM规范分别发展的事实意味着,如果EJB不吸收CCM的进步、将二者融合的话,CCM将赶上EJB)。CCM在EJB的会话构件和实体构件之外增加了对过程构件的支持。更确切地说是,CCM的会话构件相对于EJB中的有状态会话Bean,然而无状态的会话bean在CCM中被称为服务构件。过程构件的实例的状态在一次调用后不再保持。过程构件实例具有持久的状态但是不能通过主键来定位。因此,过程构件对于捕获正在进行的过程状态是有用的,但不能用于捕获可确认的实体状态。

CLR语境和通道

CLR语境的内部设施也许是第一个尝试为语境相关组装提供真正的可扩展设施的主流结构。它不像MTS,COM+,EJB和CCM容器,其所谓的语境属性的列表不是封闭的。如果语境中的对象确实需要,那么第三方就能够在语境边界上添加新的属性。这样,当调用跨越语境边界时,语境的属性能够截取、产生作用或操作任何输入和输出消息。

当构造一个新的语境时,CLR提供一个一次性的机会在语境边界上设置属性。一个特殊的语境属性可以通过编程的方式添加,比如通过其他的属性或是请求创建该属性的对象。另外,一个新的语境属性能够被创建它的对象公开地请求。公布的机制依赖于CLR的定制属性——能够直接被CLR类替换的可扩展元数据。类似于COM+属性或是EJB的部署描述,这样的定制属性需要明确的声明。比如,一个类需要同步支持。当实例化时,系统检查这个新的对象是否处于一个具有同步属性的语境中。.NET框架为COM+企业服务定义了标准的属性。这样,通过CLR-COM之间的互操作内部结构,在CLR中调用COM+的服务,创建被管理的对象就变得很容易了。

CLR对象有4种类型——值类型、传值类型、传引用类型和语境约束类型。在通过应用域(AppDomain)的边界通信时,值类型和传值类型都是使用值编排。传引用和语境约束类型则都是将应用编排。其中唯一有趣的方面在于它们创建了一个方便的编排边界。编排在通道上执行,并能加入新的通道类型。标准类型包括在SOAP/HTTP协议之上编排和在DCOM(Distributed Component Object Mode)之上编排。在通道的末端,如果系统为传引用类型的对象设置了代理则系统会重新构造传值对象。新的代理实现也可以被加入。

语境约束类型总是位于一个带有合适属性的语境中。位于语境之外的其他对象是语境“(agile)”。COM中类似的概念叫“套间(agile)”对象,但这个概念不安全。不过,在CLR中,对语境约束对象的应用不能撤销。当调用通过语境边界时,语境边界拦截所有调用者或者被调用者是语境约束对象的所有调用。例如,当在一个“(agile)”对象的域中存储一个语境约束对象的引用时,先传递该引用到另外一个语境,当通过该引用调用对象的一个方法时,该调用将会被拦截。如果该引用——通过各种方式——回到了原来的语境中,则当另外一个调用到达时,该调用将不会被拦截。

默认的情况下,新的对象放置在和请求创建新对象的对象一样的语境中。或者,新创建的对象可以选择其他的语境(不过需要该语境中另一个对象的“帮助”)或者创建一个新的语境。如果被选中的语境的属性与该对象声明的要求不匹配,则一个新的语境将自动被创建。

元组和对象空间

在所有以上方法之前的一个关于语境相关组装的方法是基于无所不在的数据空间的概念,数据空间能够在不需要明确寻址的情况下被用于通信。该工作的发起人是耶鲁大学的David Gelernter和他的小组(www.cs.yale.edu/Linda/linda.html),他们建立了该设计领域。特别要指出的是,由Nicholas Carriero和David Gelernter创建的Linda协作语言引入了元组空间的概念。那些保存有原子数据的空间就称为元组。在元组空间上,Linda仅仅定义了三种基本的操作——添加一个元组到一个空间中、在一个空间中匹配和读取一个元组、在一个空间中匹配和删除一个元组。当前这种思想的“追随者”是JavaSpaces(java.sun.com/products/javaspaces)。

协调数据和对象空间有一些附加的好处,比如我们没有必要确认构件实例之间的位置和它们的依赖性,决策安排也与数据流上的功能需求完全分开。同时,这些性质也面临着挑战。比如,为了避免在分布式实现中的集中瓶颈问题,元组空间不应该位于一个单独的物理位置。然而,为了实现高效的安排,有效的信息和被请求的数据元组或对象需要被传播至系统的每一个部分。保持元组空间操作原子性的需求使得对机构的要求进一步复杂化。

元组和对象空间对语境相关组装的求精有潜在的好处——即数据驱动的组装。在写这本书的时候,该方法已经不局限于研究项目,并影响着主流的技术。然而,有的人会说目录结构的广泛使用是元组空间融合的有趣的例子。这方面的例子包括因特网域名服务(DNS),轻量级目录访问协议(LDAP)目录,微软的活动目录和Web服务的UDDI目录。

目录假定了一个弱一致性模型,即条目很少被复制,临时的更新不一致性也是可以忍受的,条目改变的频率远远低于读条目的频率。这些性质保证了目录实现能进行大规模甚至是全局的可扩展性。比如,DNS就被实现为DNS服务器的全局层次,这些服务器联合起来以惊人的速度来处理数百万台机器的基于DNS的名字解析请求。

可以证明,所有主流的构件方法都是依靠目录来进行某种形式的语境组装,而没有哪种主流方法是像Linda协作方法所建议的那样通过构件框架来做。相反,目录服务通过一些API得以应用,而协作的工作则留给客户端构件的开发人员。

更通用的数据驱动的构件框架也是存在的。在COM+的队列构件或者J2EE的消息驱动Bean中,数据的分布就是通过面向消息的中间件(消息排队系统)来进行的。消息的到达引起相应的处理构件的自动激活,后者则通过在局部产生影响或者进一步发送消息来做出响应。

构件开发

面向构件的编程目前仍然是一门年轻的学科,其涉及的许多方面仍需要进一步地研究。本章的论述主要涉及了面向构件编程的方法学、环境和语言等三个方面。编程方法学主要考虑如何用一种系统化的方式来进行构件系统的划分、构件的交互和建造。而编程的环境和语言则主要考虑如何表现和支持特定的编程方法学。

$\color{red}{\text{面向构件的编程方法学}}$

如同面向对象的编程(OOP)关注于如何支持建立面向对象的软件解决方案一样,面向构件的编程(Component-Oriented Programming, COP)关注于如何支持建立面向构件的解决方案。一个基于一般OOP风格的COP定义如下(Szyperski, 1995):“面向构件的编程需要下列基本的支持:

—— $\color{green}{\text{多态性}}$ (可替代性);

—— $\color{green}{\text{模块封装性}}$ (高层次信息的隐藏);

—— $\color{green}{\text{后期的绑定和装载}}$ (部署独立性);

—— $\color{green}{\text{安全性}}$ (类型和模块安全性)。”

面向构件的编程仍然缺乏完善的方法学支持。现有的方法学往往只关注于单个构件本身,并没有充分考虑由于构件的复杂交互而带来的诸多困难。其中的一些问题可以在编程语言和编程方法的层次上进行解决。这其中,面向连接的编程尤其吸引了语言设计领域众多研究者的关注,例如,ArchJava(Aldrich等人,2002和Jiazzi McDermid等人,2001)。然而,面向连接的编程并不是通向面向构件的唯一途径。

分层体系结构或其他体系结构的设计方法有助于控制系统的复杂性,并能够指导系统的演化。但是,单独依靠体系结构并不能有效地指导构件及构件框架的开发活动。许多问题仍然没有得到根本的解决。主要问题如下。

1)异步问题

当前的构件互连标准大都使用某种形式的事件传播机制作为实现构件实例装配的手段。其思想是相对简单的:构件实例在被期望监听的状态发生变化时发布出特定的事件对象;事件分发机制负责接收这些事件对象,并把它们发送给对其感兴趣的其他构件实例;构件实例则需要对它们感兴趣的事件进行注册,因为它们可能需根据事件对象所标志的变化改变其自身的状态。

2)多线程

“多线程会使你寝食难安。”Swaine在后来的著作中解释,他的一些与此相似的论断具有明显的煽动性,但是他并不认为这些论断是错误的。多线程是指在同一个状态空间内支持并发地进行多个顺序活动的概念。相对于顺序编程,多线程的引入为编程带来了相当大的复杂性。特别是,需要避免对多个线程共享的变量进行并发的读写操作可能造成的冲突。这种冲突也被称做数据竞争,因为两个或多个线程去竞争对共享变量的操作。线程的同步使用某种形式的加锁机制来解决此类问题,但这又带来了一个新的问题:过于保守的加锁或者错误的加锁顺序都可能导致死锁。

多线程主要关注于对程序执行进行更好的分配,发送并发请求的客户端能够很好地观察到这种分配。然而,获取性能最大化的手段却根本不依赖于多线程,而是尽量在第一时间内以最快的速度处理用户的请求。即使能够避免死锁,同步也可能导致一定程度的性能损失。必须避免对经常使用的共享资源进行不必要的加锁。跨线程的异常传播也会导致处理非同步的异常变得更加困难。而且,使用多线程和复杂的互锁机制将使得代码调试变得异常困难。

显然,在真正并发的环境下,这些问题无一不需要考虑。例如,如果构件实例运行在独立的处理器上,就需要考虑并发请求的问题。可以在处理一个请求时对某个构件实例进行完全的加锁,但这样做可能会导致死锁或者糟糕的响应时间。

3)“生活”在没有实现继承的状态下

构件间的实现继承所引起的严重问题使得人们倾向于使用简单对象组合或消息转发来替代实现继承。但是,当我们仅需要对已有的实现进行轻微的修改时,这种替代方式却又显得太笨拙。创建一个具有很多方法的类的子类,且只重写父类中一小部分方法是很容易实现的。相对而言,仅为转发一小部分方法调用而生成一个新的包装类却是一件十分繁琐的事情。除了实现上的开销以外,简单的转发还增加了运行时的开销(执行时间和代码占用空间的增加)。

当一个对象中的方法被分组成若干个接口,每个接口中含有数目恰当的方法时,COM风格的聚合有助于避免由于转发所带来的性能损失。通过使用多种自动化技术,对于程序员而言,其实现成本没有丝毫的增加。

一种解决方案是根据转发目标对象的接口生成转发类的代码。这种方案的弱点也是所有代码生成途径所共有的:目标对象接口的改变要求重新生成转发类的代码,或者手工调整旧的生成代码。

另外一种解决方案是利用模板机制(如C++中的模板)在编译时刻生成所需的代码。模板可以通过参数化的方式配置,而不必手工编辑生成的代码。编译器根据模板的实例化参数来生成最终的代码。

4)坚壳类

第三种解决方案是使用实现继承。虽然一般来讲实现继承具有严重的问题,但对于白盒类(以完整的源代码形式发布并且不再被改变的类)使用实现继承是没有问题的。对经常成为转发目标的构件接口来说,可以为其关联一个专门负责处理转发者琐碎细节的坚壳类(Szyperski, 1992b)。坚壳类与转发目标具有相同的接口,而且所有方法的实现都是简单地向目标转发消息。坚壳类本身是抽象的,尽管所有的方法都有实现,但是这种实现完全没有引入新的功能。(有趣的是,某些语言包括C++,无法表现这样一个事实,即一个没有抽象方法的类仍然是抽象类。)然而,可以通过继承坚壳类去截取某些方法调用,从而产生一个有意义的转发者。由此导致的程序员工作量与一般的实现继承类似,但产生的效果是转发而不是代理。

Java中的代理类(proxy class)就是这样的一种机制。一个代理看起来是某种给定的类型,但其内部却实现为代理类的一个子类。这种实现提供了一种对调用进行截取的机会。CLR通过实时代理类也提供了类似的机制。

5)语言支持

第四种解决方案是语言支持,这种解决方案或许更易于被接受。如果编程语言直接支持转发类的构造,则所有以上方案的缺点都可以被避免。编程的开销也将是最小的,且在运行时刻时间和空间上的开销与实现继承方式相比也都是一样的。但目前还没有主流的编程语言来支持这种构造。比如,C++的虚拟基类机制不允许在几个独立的对象之间共享基类对象。它也不允许动态改变基类对象,或虚拟基类的独立子类化。Objective-C(Apple Computer, 2000; Pinson和Wiener, 1991)是一种支持对象动态继承的非主流编程语言。

6)调用者封装

语言支持带来的另外一个好处是接口定义。当构件对外提供一个接口时,可能会涉及两种不同的意图。一方面,构件外部的代码可能会调用这个接口中的操作。另一方面,构件内部的代码可能需要调用实现这个接口的一些操作。在COM技术中,这体现为入接口和出接口的差异。除了Component Pascal以外,没有别的语言能够恰当地支持构件的纯出接口。

正如许多传统的封装机制一样,如果出接口和入接口之间的对称可以被接受,那么仅对调用者而不是对被调用者进行封装也就不应该令人感到惊奇。然而,适合于构造构件的调用者封装机制被大多数的语言丢弃了。在类似Simula的语言,包括Beta语言(Lehrmann Madsen等人,1993),均支持内部方法。在类的层次上,这种机制和调用者封装很相似。引介基类(Introducing Base Class)之外的代码无法访问由子类所实现的内部方法。每个人都可以试图调用该方法,但是基类代码的执行受到了保护,至少能够动态地防止非法的外部调用者。

调用者封装策略被应用于黑盒构件框架的若干方面。例如,只有框架可以调用视图的关键方法。如果框架在早些时候捕获了对同一个视图的同一个方法调用产生的异常,它将会阻止对此方法的进一步调用。这样,产生错误的视图的某些方法将被屏蔽,从而不会再继续扰乱系统的运行。黑盒是很少几个能够保证嵌在复合文档中的视图不会破坏文档的整体性的系统之一。

环境与选择目标框架

脱离了良好定义的环境,一个构件实例是不能正常工作的。构件框架定义了这样的环境。然而,一个构件实例可能被设计成可以在多个构件框架中工作。根据构件系统体系结构的不同,框架可以根据不同的角色被分割成不同的子框架。例如,每个框架都可能会采用某种特定的机制实现构件之间的协同运作。在这种情况下,分布式框架可能会负责在机器之间分发构件实例。而另外一个单独的框架将会负责对复合文档的集成。构件的设计可能需要考虑所有的这些子框架,以使得最终的构件实现能够在这些框架下正常运行。

工具与选择编程语言

原则上,构件编程几乎可以使用任何一种语言,并采用任何范型。并不存在所谓的最低需求。构件编程主要关注的是对相关构件的多态处理。由于构件之间的交互需要动态进行,因此就必须支持后期绑定。参数构造安全性还需要封装及安全——类型安全和模块安全——的支持,在大多数情况下垃圾回收的支持也是必需的。此外,构件编程需要一种能够显式化声明状态依赖的机制,理想情况下应该保持这些依赖是可以参数化的。对实现中的依赖进行完全的参数化导致了面向连接的编程。在语言范型的层次上,面向对象范型最接近于面向构件的编程范型,但是其他的范型,比如功能范型,可能也是合适的。

到目前为止,只有少数编程语言在应用层次上支持面向构件的编程。许多流语言,如COBOL、Object COBOL、FORTRAN、C、C++ Pascal和Smalltalk在不同程度上均缺少对封装、多态、类型安全性、模块安全性的支持。

Java、C#和Component Pascal都分别支持在包一级或者模块一级的访问保护。通过这种方式,可以建立对模块安全性的支持。但Java的开放包机制对模块安全性的支持太过脆弱。即使不使用替换目标文件的方式,也可以通过向包中添加新的类,从而完全穿越包机制提供的保护!这种漏洞需要通过另外的途径来弥补。因此,需要把包放在文件系统中的保护目录下,或者其他带有访问控制的地方。在一个从远端服务器动态获取类文件的环境中,这种情况将变得更为复杂。需要一种机制来保证同一个包中的类文产生于同一个编译源。为了支持更加开放的设置,Java或许需要采用封闭的模块构造机制,其中每一个这样的模块被映射到一个被发布的编译文件上。Java的嵌套类机制有助于建立真正的所谓模块,但是Java缺少一个能够支持对一个类及其嵌套类的访问保护层。另外,由于JVM实际上并不真正支持嵌套类,Java编译器不得不把嵌套类抽取出来,放入单独的类文件中。

C#令人感兴趣的地方在于其模块级访问保护适用于集合。C#的任何构造机制(事实上任何基于CLR的语言都适用)可以被打包成为一个集合。一旦被打包,该集合也就被加密了,从而避免对其的任意窜改,这也使得对集合内部的访问控制机制变得强大和有效。因此这种基于CLR的集合内部访问机制是目前为止最灵活的包概念。

构件组装

构件是可被第三方独立部署的基本单元。每个构件的部署过程之间不是相互孤立的,构件实例之间通常会在一个或多个构件框架的介入下发生交互。将构件组装成系统的一种显而易见的方法是通过传统的编程方式进行。然而,由于这种方式支持用较简单的方式生成大多数常用的构件系统(或由于其能够完全避免单独的组装过程),因此构件的适用范围和生存能力都大大地增加了。

构件初始化及互连

体系结构描述语言(ADL)即遵从了这样的思想:这些语言通常都把构件和连接子作为其核心的建模概念。组装因此表现为选择一组构件并通过适当的连接子将这组构件进行连接的过程。基于这种方式的组装过程实际描述的是被选择的构件的“实例”应该如何通过适当的连接子“实例”互连的过程。这个细节揭示的一个重要之处在于:构成一个组合体的基本元素是构件或连接子的实例而不是构件或连接子本身。例如,虽然概念上一个构件可以出现在两个组合体中,但实际上是这个构件的不同实例将出现在这两个组合体的若干实例中。

BML(bean markup language)是种由IBMalphaWorks实验室在1998年发布的针对JavaBean的构件组装语言。BML基于XML并针对JavaBean构件模型进行了定制。通过使用XSLT,可以从更抽象的系统描述中生成BML。实际上,BML自身与JavaBean构件模型已相当接近,能够支持Bean构件实例的创建、访问及配置等操作。为了支持配置,BML允许对Bean属性进行访问和设置。当提供了具有这种配置方式的Bean构件实例后,BML能够用来绑定Bean构件,使其作为监听者监听其他Bean产生的事件。BML既可以通过直接产生配置后的可运行子系统而被解释,也可以被编译而生成Java代码。BML解释器的基础是bean定制化框架。该框架也能够支持实现不同于BML的其他形式的bean配置和互连语言。

1)构件的可视化组装

构件实例的可视化组装方式能够有效地简单化组装过程。例如,JavaBean构件能够区分其实例的使用和构造阶段。一个bean因此可以表现出特定的外观(例如,一个类似建筑单元的图标)、行为(例如,一个可以和其他实例连接的句柄)和帮助信息(例如,针对特定人员的构件组装帮助文档)。在组装过程中,构件被实例化,实例通过统一的方式把其具有的出接口和入接口连接到相关的实体上。JavaBean和COM技术均支持这种一般方式的连接范型。

2)用复合文档取代可视化组装

在构件实例可见的情况下(通过提供一个可视化的用户界面),专业的构造器或组装环境可以和一般用途的软件开发环境相统一。而通过复合文档,构造和使用这两个不同的环境也可以自然直接地集成在一起(文档代表应用系统),对文档的编辑相当于构件(实例)的组装过程。在这样的系统中,构件的组装者和使用者之间不存在任何的隔阂。这两者之间的平滑过渡就如同在已有构件组合体的基础上,通过后期组装生成新组合体,与通过编程生成新组合体可以随意地相互结合使用,以满足特定应用系统的需要一样。因此,为了全面地满足使用者的需求,组装机制应该具有在使用时刻的可用性。黑盒构件构造器及构件框架即遵循了这样的途径。虽然我们可以通过不部署所需的构造器构件的方式来区分构件的组装和使用,但复合文档并不对这两者做严格的区分。

构造环境和使用环境的无缝集成(特别是在复合文档的方式下)也形成了对于快速应用开发(Rapid Application Development, RAD)的强有力支持。在这样的环境中,工业级构件、原型构件及一次性解决方案可以自由地结合。需求捕获和对需求改变请求的确认也可以高效且有效地执行。如果企业或组织需要,经过相应的培训后,最终用户可以进一步地调整他们的系统。

3)非图形用户界面环境的构件

大多数早期的构件化软件方法往往关注于客户端的前台交互式应用系统。现代图形用户界面的需求本质,加上用户界面的相对规则性,使得与用户界面相关的可重用构件成为具有独特价值的软件资产。然而,计算的其他领域,特别是基于服务端的解决方案,存在同样甚至更多的复杂性,而且已经引起了现阶段许多构件化软件方法的关注。

对基于服务的构件,构造和使用阶段的清晰划分显得更为自然。Oliver Sims于1994年提出的业务对象,是最早提出的针对“无处不在的构件”(Components Everywhere)思想的建议之一。接下来的一个重要进展则是Java servlet的出现。Java servlet是运行于服务器上的构件,但它们仍可以通过可视化的方式来进行组装。为了与现存的构件模型良好协调(包括CORBA提出的构件模型),使用前的组装通常需要较早地做出某些决策,如分布式系统中构件实例的分布决策。值得注意的是,虽然对象迁移能够带来的益处仍然值得讨论,但大多数系统,包括当前的CORBA实现,都支持持有其他对象引用的对象的迁移能力。然而最近的一些围绕Web服务的途径(基于SOAP),却不支持对远程对象引用的传递。相反,SOAP主张对定位器(Locator)的传递,如URL或COM中的moniker。这些定位器在不同的机器上每次可能会被解析到不同的对象上。因此,远程对象标志这个概念在SOAP和Web服务中并不存在,迁移问题也由此变得非常简单。

关于服务端构件模型的典型解决方案包括适用于应用服务器的EJB模型(Sun公司J2EE的一部分)和COM+模型(微软公司),以及适用于Web服务器的servlet模型(基于Sun公司JSP技术)和Visual Basic及其他技术(基于微软公司ASP技术)。微软的.NET框架还引入了一种新的同时适用于客户端和服务端的基于CLI(Command Line Interface)的构件模型。

4)可管理且“自引导的”构件组装

构件的组装实际上是指对构件实例的组装(一个采用对象技术实现的构件实例通常是一个由若干对象形成的消息网络)。当然,通过组合已有构件来实现新构件也是可行的,这种方式类似于传统的基于底层函数库构建高层函数库的过程。换而言之,构件组装(不是构件实例)只不过是编程的一个代名词,而构件实例的组装并非如此。构件实例组装提倡把实现构件的代码和资源与“连接”构件实例的代码这两个方面保持分离。构件实例的连接可以通过轻量级编程的方式(如编写脚本)来实现,而新构件的编写则应采用其他的方式(脚本语言或接口语言并不适合编写构件,因为编写构件与连接构件实例有本质的不同)。

5)最终用户组装

当需要时,允许最终用户进行系统组装以获得高度定制的解决方案是非常有价值的。最终用户的参与导致产生了一个有趣的、介于完全自引导和完全静态预定义之间的构件组装模式。显然,即使有最终用户的参与,组装过程仍然需要一定程度的自引导性。我们不应当期望用户能够完全承担技术细节层次上的构件组装工作。

系统组装分为三个不同的层次:定制(customization)、集成(integration)和扩展(extension)。这三个层次对应于构件组装过程中的不同任务。但这种最终用户剪裁仅仅是从用户的角度观察其关心的领域问题,而不是从构件组装的技术角度来进行的。Robert Slagter和Henriter Hofte(2002)展示了这种思想在计算机支持的协作应用软件系统中的一个有趣应用。该系统支持最终用户根据需要去组合群件的行为。Groove Transceiver(www.groove.net)具有的类似特征则允许最终用户通过选择和配置工具快速地组装工作空间。

6)构件演化

构件技术体现了一种后期组装的思想。构件的逐渐成熟会进一步推后组装(或绑定)时间,但随之而来的是整个系统将变得越来越脆弱。构件通常也会经历一般软件产品具有的演化过程。

安装新版本的构件将会与期望使用旧版本构件的现有系统发生冲突,甚至直接与现存的旧版本构件实例发生冲突。相对于已经实例化的构件,一个构件从构件库中被获取并实例化的时间越晚,潜在的版本冲突问题就会越严重。

在分布式系统中,为安装新版本的构件实例而终止所有现有构件的运行是不现实的。不同版本的客户端和不同版本的构件实例之间的二进制互操作性需要在版本间二进制兼容性中就加以考虑。如何实现构件实例的在线版本升级仍然是一个非常活跃的研究领域。

在实际配置中,必须考虑构件的不同版本实例共存于一个系统的情况。系统的升级就是一个重要的例子。除采用多版本共存技术之外,解决“遗留系统移植”问题还需要通过使用包裹器构件来适配旧版软件或解决系统不兼容性。

支持版本共存和包裹器构件技术的方法之一是COM所使用的方法。按照约定,一旦COM接口被发布,其就不能再被更改。因此,COM中不存在针对单个接口的版本问题。一个提供新版本服务的构件将不得不使用一个新的接口。采用这种方式的一个重要优点是它使得同时支持具有不同语义的新旧接口成为可能。显然,一旦一个接口不再被支持时,该接口就可以从系统中安全删除。

CORBA采用的版本管理机制的能力则较弱,它仍然试图将所有版本的所有操作合并成一个接口。因此,这种方式不支持仅改变操作的语义而不改变该操作名称或原型的能力。由此导致的一个后果是,虽然可以引入操作的新名称,但若想删除一个旧的操作而不改变二进制兼容性却非常困难。甚至SOM的版本序列机制(SOMs Release Orders)也无法解决这个问题。

什么是corba