摘要:构件技术蕴涵了太多概念。一个典型的例子就是含义众多的术语"对象".随着时间的推移,模块、类和构件的概念都最终为"对象"所包括。最近,术语"软件构件"甚至到了把以前的普通对象也称作是构件的程度。将几个术语合并为一个,看起来似乎方便了使用,但除此之外再无其他好处。所以,必须在保证术语的准确度和直观性的前提下取得某种平衡。下面定义了几个关键的术语,并描述了它们之间的关系。
9.1.1术语与概念
构件技术蕴涵了太多概念。一个典型的例子就是含义众多的术语"对象".随着时间的推移,模块、类和构件的概念都最终为"对象"所包括。最近,术语"软件构件"甚至到了把以前的普通对象也称作是构件的程度。将几个术语合并为一个,看起来似乎方便了使用,但除此之外再无其他好处。所以,必须在保证术语的准确度和直观性的前提下取得某种平衡。下面定义了几个关键的术语,并描述了它们之间的关系。
1.构件
构件的特性如下。
独立部署单元D作为第三方的组装单元。
没有(外部的)可见状态。
这些特性有几重含义。一个构件是独耷可部署的,意味着它必须能跟它所在的环境及其他构件完全分离。因此,构件必须封装自己的全部内部特征。并且,构件作为一个部署单元,具有原子性,是不可拆分的。也就是说,第三方没有权利访问其所使用的任何构件的内部细节信息。在这样的约束下,如果第三方厂商能将一个构件和其他构件组装在一起,那么这个构件不但必须具备足够好的内聚性,还必须将自己的依赖条件和所提供的服务说明清楚。换句话说,构件必须封装它的实现,并且只通过良好定义的接口与外部环境进行交互。
最后,一个构件不能有任何(外部的)可见状态--这要求构件不能与自己的拷贝有所区别。但对于不影响构件功能的某些属性,例如,用于计费的序列号,则没有这种P艮制。通过对属性的可见性进行限制,允许用户在不影响构件的可见行为的前提下,使用合法的技术手段对那些影响性能的状态进行特殊处理。特别是,构件可以将某些状态专门用于缓存(缓存具有这样的特性:当它被清空时除了可能会降低性能以外,没有其他后果)。
构件在特定的系统中可以被装载和激活。但是,由于构件本质上没有状态,因此,在同一操作系统进程中装载多个构件的拷贝是毫无意义的,而且它们之间是不可区分的。也就是说,给定一个进程(或者其他的语境),至多会存在一个特定构件的拷贝。因此,谈论某个构件的可用拷贝的数量是没有什么意义的。
在目前许多系统中,构件被实现为大粒度的单元,系统中的构件只能有一个实例。例如,一个数据库服务器可以作为一个构件。如果这个服务器刚好只维护了一个数据库,那么会很容易把该数据库误认为是实例,如公司里的员工工资管理服务器。该数据库服务器连同其中的数据库,可以被视为一个有可见状态的模块。根据上面的定义,该数据库并不是一个构件,但那个静态的数据库服务器程序却是一个构件一它只支持一个数据库"对象"实例。也就是说,在这个例子中,工资管理服务器程序是一个构件,而其中的工资数据只是实例(对象)。这种将不易变的"模型"和易变的"实例"分离的做法避免了大量的维护问题。如果允许构件拥有可见状态的话,那么任何两个来自同一个构件的实例都不会拥有相同的属性。
在这一点上一定要分辨清楚。这里所说的构件的概念与对象层次上的可见或不可见状态无关,也与对象状态^生命周期(每次呼叫,每次会话,或是一直的)无关。这些全都是对象层次上所关心的东西,与构件的概念并没有直接的关系,但是通过构件,我们可以获得拥有任何这些属性的对象。
2.对象
说起对象,不得不提实例化、标志和封装。与构件的特性不同,对象的特性是:
一个实例单元,具有标志。
可能(具有状态,此状态外部可见。
封装,了自己的状态和行为。
同样,对象的一系列属性随之而来。由于对象是一个实例化的单元,所以不能被局部初始化。由于对象有各自的状态,它必须有标志,以使它在整个生命周期内,无论状态如何变化,都能够被地识别。
当对象被实例化的时候,需要一个构造方案来描述其状态空间、初始状态和新生对象的行为。该方案在对象存在之前就己经存在。显式存在的实例化方案称为类。也有隐式的实例化方案,即通过克隆一个已存在的对象来实现,即原型对象。
无论使用类的形式,还是用原型对象的形式来初始化一个对象,这个新生的对象都必须被设置一个初始状态。创建与初始化控制对象的代码可以是一个静态的过程一如果它是对象所从属类的一部分,就被称为构造函数。如果这个对象是专门用来创建与初始化对象的,则简称为工厂。对象中专门用来返回其他新创建的对象的方法常被称为工厂方法。
3.构件与对象
构件的行为显然可以通过对象来实现,因此构件通常包含了若干类或不可更改的原型对象。除此之外,构件还包括一系列对象,这些对象被用来获取默认的初始状态和其他的构件资源。
但构件并非一定要包含类元素,它甚至可以不包含类。实际上,构件可以拥有传统的过程体,甚至全周变量,它也可以通过函数语言,或者汇编语言,或者其他可用方法实现自身的全部特性。构件创建的对象--更确切地说是对这些对象的引用--可以与该构件分离开来,并对构件的客户可见。构件的客户通常是指其他的构件。除非构件的对象对客户可见,否则我们无从判断一个构件内部是否是"完全面向对象"的。
-个构件可以包含多个类元素,但是一个类元素只能属于一个构件。将一个类拆分进行部署通常没什么意义。另外,正如类之间可以通过继承关系等产生依赖一样,构件之间也可以存在互相依赖的关系--这种依赖很重要。一个类的父类并不一定与它的子类存在于同一个构件中。如果一个类的父类存在于外部的其他构件中,那么这两个类之间的继承关系便是跨构件的,这种关系将会导致相关联的构件间的导入关系。规约的继承是保证正确性的一项很关键的技术,因为共同的规约是构件间达成共识的基础。至于构件间对实现的继承是好是坏,仍然是众多学术流派争论的焦点。
4.模块
构件与模块的概念其实非常类似。模块的概念出现于20世纪70年代后期的模块化语言(Wirth,1977:Mitchell等人,1979)。最广泛使用的模块化语言是Modula-2(Wirth,1982)和Ada.在Ada里面,模块被称做包,但其实两者是相同的。模块化方法成熟的标志是其对分离编译技术的支持,包括跨模块的正确的类型检查能力。
随着Eiffel语言的面世'类被认为是更好的模块(Meyer,1988)。这似乎是正确的,因为我们最初的想法是每个模块实现一种抽象数据类型。毕竟,我们可以把一个类看成是一个抽象数据类型的实现,只不过它多了继承和多态的特性而己。然而,模块常常被用于把多个诸如抽象数据类型、类等实体打包到一个单元中。并且,模块没有实例化的概念,而类却有。
在其后出现的程序设计语言中--比如Modula-3,ComponentPascal,和C#--模块的概念(C#中的集合)与类的概念是区分对待的。在任何情况下,模块都可以包含多个类。在有些没有模块概念的语言(诸如Java语言)中,模块可以通过嵌套类来模拟实现。类之间的继承关系并不受模块界限的限制。另外值得一提的是,在Smalltalk系统中,经常会通过修改当前已存在的类来构造一个应用程序'人们巳经开始尝试定义"模块系统",这将使Smalltalk越过类而直接达到构件级水平,例如Fresco(Wills,1991)。
模块本身就可以作为一个最简单的构件。即使不包含任何类元素的模块也可以实现构件的功能。传统的数值计算函数库就是一个很好的例子,这些库是功能性的,而不是面向对象的,但却可以打包成模块。然而,一个成熟的复杂的构件却并不是简单地仅用模块就可以实现的。模块没有持久不变的资源,有的只是那些被硬编码到程序中的常量。资源可以参数化一个构件。通过替换这些资源,就可以重新配置该构件而无需更改构件代码。例如,本地化设置可以通过资源配置实现。看起来资源配置好像为构件赋予了可变的状态值。但是我们知道,构件不能修改它们自身的资源,这些资源与编译后的代码一样只是构件的组成部分。追踪一个构件与它所派生的本地化了的构件之间的关联,在某种程度上,和追踪同一构件的不同的发布版本之间的关系相似。
某些情况下,模块并不适合作为构件,掌握这些情况是非常有用的。根据本书的定义,构件没有外部可见的状态,但是模块却可以显式地用全局变量来使其状态可见。并且,通过直接导入其他模块的接口,模块之间可以存在静态的代码依赖。而对于构件来说,虽然也允许存在对构件外部代码的静态依赖关系,但却并不提倡。这种静态依赖关系应被限定用于那些合约元素,包括类型和常量。使用间接而非直接的接口表示模块的依赖关系,把对实现代码的依赖关系限定于对象层次,就可以利用同一接口的不同实现来灵活地组装模块。
总的说来,模块化是构件技术产生的前提,但对于构件来说,传统的模块化的概念和标准是远远不够的。很多模块化的概念源自Pamas(1972),其中包括最大化内聚性与最小化耦合性这条基本原理。因此,模块化的思想并不新鲜。但遗憾的是,现今的大部分的软件仍然不是模块化的。比如,有不少的大型企业应用都是对一个单一的数据库进行操作,允许应用系统的任何一部分依赖于数据模型的任何部分。但构件技术则要求系统中各部分必须互相独立,或者存在可控的显式依赖关系。因此构件技术必将导致模块化的解决方案。这种软件工程效益充分说明对构件技术的投资是有价值的。
5.白盒抽象、黑盒抽象与重用
黑盒抽象与白盒抽象的区别主要在于接口"后面"的实现细节是否可见。在理想的黑盒抽象的情况下,客户对接口和规约之外的实现细节一无所知。而在白盒抽象中,在接口限制了用户行为并确保了封装性的情况下,客户仍然可以通过继承对构件的实现细节进行修改。由于在白盒方式中实现细节对外界是完全可见的,因此可以对实现细节进行研究,以加深对该接口抽象含义的理解。
揭示实现细节的可控部分。这是一个有争议的概念,因为部分可见的实现细节可以是规约的一部分。一个完整的实现只需要保证,能被客户看见的那部分实现细节与抽象
的接口规约一致即可。这是将规约实现的标准方式。
黑盒重用指仅仅依赖接和规约来实现。比如,在绝大多数系统中,应用程序接口(Application Programming Interface,API)完全与内部的具体实现无关。用这样的应用程序接口构造系统相当于黑盒重用这些接口的实现。
相反,白盒重用指依赖于对具体实现细节的理解,通过接口来使用软件部件。大部分类库和框架都会提供源代码,应用程序开发人员通过学习类的具体实现,就可以知道如何构造该类的子类。
在白盒重用中,被重用的软件不可以轻易地被另外的软件替换。如果贸然替换将有可能破坏正在重用的客户端,因为这些客户端依赖于那些在未来可能发生改变的实现细节。
根据上述特性可以得出以下的定义:"软件构件是一种组装单元,它具有规范的接口规约和显式的语境依赖。软件构件可以被独立地部署并由第三方任意地组装。"
这个定义最先是在1996年的面向对象程序设计欧洲会议上(European Conferenceon Object-Oriented Programming,ECOOP),由面向构件程序设计工作组(Szyperski和Pfister,1997)提出。该定义涵盖了我们之前讨论的那些构件特性。它既包括了技术因素,例如独立性、合约接口、组装,也包括了市场因素,例如第三方和部署。就技术和市场两方面的因素融为一体而言,即使是超出软件范围来评价,构件也是独一无二的。
而从当前的角度看,上述定义仍然需要进一步澄清。一个可部署构件的合约内容远不只接口和依赖,它还要规定构件应该如何部署、一旦被部署(和启动)了应该如何被实例化、实例如何通过规定的接口工作等。事实上,各个接口的规约都应该被独立地看待,任何提供与使用该接口实现的构件之间都是相对独立的。比如,一个实现队列操作的构件通过一个接口获得物理存储空间,通过另外两个接口提供入队列和出队列的操作。在构件的合约中说明,通过入队列接口插入队列的元素,可以通过出队列接口中的操作取出来,这种关联关系,任何接口规约都不能单独提供。该合约同时也规定构件一旦被实例化,就必须在关联一个实现了物理存储空间接口的构件之后才能被使用。这种关联将受到底层构件模型的组装规则的影响。具体的部署和安装的细节由特定的构件平台提供。
6.接口
接口是一个己命名的一组操作的集合。构件的客户(通常是其他构件)通过这些访问点来使用构件提供的服务。通常来说,构件在不同的访问点有多个不同的接口。每一个访问点会提供不同的服务,以迎合不同的客户需求。强调构件接口规范的合约性非常重要,因为构件和它的客户是在互不知情的情况下分别独立开发的,是合约提供了保证两者成功交互的公共中间层。
成功的合约接口需要遵循哪些非技术因素?首先,必须时刻关注经济效益。一个构件可以有多个接口,每一个接口提供一种服务。有一些服务会格外受客户欢迎,但是如果所有服务都不受欢迎,那么服务的组合也不会受欢迎,这个构件就没有市场价值了。这样的话,就没有必要在非构件的实现方案的构件化上进行投资了。
其次,应当避免不当的市场分化,因为这威胁到构件的生存。所以,尽量不要重复引入功能相近的接口。在市场经济中,这通常是主要生产商在市场早期努力推行标准化的结果,或者是经过残酷的市场竞争优胜劣汰的结果。但是,前者可能会由于笨拙官僚的"委员会设计"问题而不能达到最优;而对于后者,市场竞争的非技术本质也可能导致结果不是最优的。
最后,为了使一个接口的规范和实现该接口的构件得到广泛应用,需要有一个公共传媒来向大众进行宣传和推广。要做到这一点,至少需要几种能被广泛认可的保证命名性的命名方案。接口标准化的一个非常有意思的变种,是对消息的格式、模式和协议的标准化。它不是要将接口格式化为参数化操作的集合,而是关注输入输出的消息的标准化,它强调当机器在网络中互连时,标准的消息模式、格式、协议的重要性。这也是因特网(IP、UDP、TCP、SNMP等)和Web(HTTP,HTML等)标准的主要做法。为了获得更广泛的语义,有必要在一个单一通用的消息格式语境中标准化消息模式。这就是XML的思想。XML提供了一种统一的数据格式。
7.显式语境依赖
在上文的构件定义中,构件除了要说明所提供的接口外,还要说明其需求。也就是说为了使构件正常地工作,必须说明其对部署环境的具体要求。这些要求被称为语境依赖,指的是构件组装和部署的语境,包括了定义组装规则的构件模型和定义构件部署、安装和激活规则的构件平台。如果只存在一种软件构件体系的话,那么只需要列举该构件所需的所有其他构件提供的接口,这样就足够可以说明全部的语境依赖。例如,一个合并邮件的构件会声明它需要一个文件系统的接口。但是,今天的大多数构件即使连这样的需要的接口也通常不进行声明。而构件提供的接口更受关注。
事实上,目前有几种构件体系同时存在,它们相互竞争,彼此冲突。例如,现在就有OMG的CORBA,Sun的Java,以及微软的COM和CLR(Common Language Runtime)等体系。并且,因为要支持不同的计算和网络平台,构件体系本身并不是单一的。这种状况还没有很快改善的迹象。而另外一种观点则认为,所有这些构件体系攀终都只能归结为两类--CORBA+Java体系和微软体系(包括COM+和。NET/CLR)。但即使构件体系被刻意减少到不能再少的区区两种,在具体实现层次上还是存在着千差万别的。
8.构件的规模
显然,构件只有在提供了"恰当"的接口集,以及对语境依赖没有严格限制的情况下,该构件可以在所有的构件体系中运行,并且其依赖的接口不会超出那些构件体系所能提供的范围时最好用。然而,只有极少的构件拥有这么弱的环境依赖性。技术上来说,一个构件可能和它所需要的所有软件捆绑起来被提供,但这显然违背了使用构件的初衷。注意,环境需求往往取决于构件运行的目标机器。如果是虚拟机,例如Java虚拟机,这就显然是该构件体系规范的内容之一。如果是本地代码平台,仍然有类似于Apple的将多个二进制文件打包成一个文件的FatBinaries这样的机制,可以使构件在"所有地方"运行。
构件设计者通常不会构造自给自足的构件,将所需的所有东西都打包进来,而是采取一种"最大化重用"的策略。为了避免在构件中重复实现那些次要的服务,设计师通常会只实现该构件的核心功能,然后重用其他所有的一切。面向对象的设计有向这种极端发展的趋势,许多面向对象的方法论者都大力提倡这种最大化重用的思想。
虽然最大化重用的思想有很多为人称道的优点,但是它也有一个潜在的缺点--语境依赖的爆炸性増长。如果构件在发布后其设计一直冻结不变,同时所有的部署环境也都一样,那么这个问题就不会出现。然而,构件会不断地演化,不同的部署环境会提供不同的配置,多种版本会同时存在,在这样的情况下大量的语境依赖只会使构件成为众矢之的。语境依赖越多,能满足构件环境需求的客户构件就越少。总之,最大化重用降低了可用性。
构件设计者需要为以上两者找到一个平衡点。当要描述构件的基本接口的时候,设计者们就需要做出抉择。增加语境依赖通常会使构件因重用而简洁,但却会降低可用性。此外,还必须考虑环境的演化会使构件更加脆弱,例如引入新版本带来的变化。增加构件的自给性可以减少语境依赖,增加可用性,并且使构件更健壮,但却会使构件规模过大。
软考备考资料免费领取
去领取