本章主要知识点
❑ 什么是软件工程模型?CIM、PIM和PSM的区别是什么?
❑ 常见的软件工程方法有哪几种?它们各有什么优势和不足?
软件工程最核心的技术是软件建模。本章从软件工程模型出发,介绍了各种软件工程方法,包括结构化方法、面向对象方法、基于构件的开发方法、面向服务方法、模型驱动开发方法、软件产品线工程和形式化方法等。每种方法适合不同的软件类型和开发要求,有各自的软件建模技术。软件工程方法的发展呈现出三个趋势:模块化、复用、高质量。从模块化角度来看,软件模块从函数(结构化方法)、类(面向对象方法)、构件(基于构件的方法)发展至服务(面向服务方法);从复用角度来看,从代码的复用、构件(基于构件的方法)的复用,到架构(软件产品线)的复用和服务(面向服务方法)的复用,从单个产品到产品簇(软件产品线)的复用;从质量角度来看,从工程方法发展到形式化方法。
模型是对客观世界的某种简化,是对事物或系统的一种抽象描述。人们常常在正式建造实物或解决复杂问题之前,首先建立一个简化的模型,剔除那些非本质的东西,以便更透彻地了解问题的本质,抓住问题的要害;然后在模型的基础上进行分析、研究、改进和验证,最后才具体实施。对于一个工程,模型具有以下作用:
1)在正式启动工程项目之前,通过模型的分析和实验,能发现设计中的错误和遗漏之处,降低工程的风险;
2)通过模型,研究和比较不同的解决方案,选出最合适的方案;
3)用于与项目组其他成员、客户、领导等相关人员进行交流;
4)指导和促进工程的实现。
例如,建筑模型用于向客户展示建筑物的整体结构,飞机模型用来进行风洞实验等。软件系统也一样,需要在编码前构造业务模型、分析模型和设计模型。
一个好的模型应当刻画问题的关键方面,而略去其他相对次要的因素。同时,模型还应反映出各个建模者和使用者的不同视角。不同主体对同一客体的认识结果依赖于各自的视角,这样能更好地集中注意力,有效地解决关键问题。
模型可以用不同的语言和工具来描述。如果模型是为了方便相关人员交流,那么就要选用尽可能通用的、易于理解的语言和草图来描述模型。而如果一个模型要被用于在人和计算机之间交流,那么它必须是用精确定义的语言来描述的。
模型是多层次的,我们可以建立不同详细程度的模型。在软件工程领域,按抽象层次,模型通常分为 :
1)计算无关模型(Computation Independent Model, CIM)。CIM描述了一个系统的需求,以及这个系统将要被使用的业务语境。这个模型通常描述系统的用途,而不是其该如何被应用。它一般用业务语言或者领域相关语言来表达。系统的业务模型就是一种CIM。
2)平台无关模型(Platform Independent Model, PIM)。PIM是具有高抽象层次、独立于任何实现技术的模型。PIM描述了系统将会如何被创建,不涉及具体的实现技术,它并不描述针对某一具体平台的解决机制。一个PIM可能适用于平台甲而不适用于平台乙,它也可能适用于所有的平台。系统的分析模型就是一种PIM。
3)平台相关模型(Platform Specific Model, PSM),又称平台特定模型。PSM是关联于某一具体技术平台的模型。它既包含来自描述CIM该如何实现的PSM的细节问题,又包含描述如何在一个具体平台上实现这一应用的细节问题。系统的设计模型就是一种PSM。
结构化方法(Structured Method)是由Edward Yourdon和Tom DeMarco等人在20世纪70年代中后期提出的一种系统化开发软件的方法,并在20世纪80年代成为主流。该方法基于模块化的思想,采用“自顶向下,逐步求精”的技术对系统进行划分,包括结构化分析、结构化设计和结构化编程。其中结构化编程是指采用顺序、选择、重复三种基本控制结构构造程序,避免使用goto语言。本节将介绍结构化方法中的建模技术,即结构化分析和结构化设计(包括概要设计和详细设计)。
结构化分析先把整个系统表示成一张环境总图,标出系统边界及所有的输入和输出,然后由顶向下对系统进行细化,每细化一次,就把一些复杂的功能分解成较简单的功能,并增加细节描述,直至所有的功能都足够简单,不需要再继续细化为止。
结构化分析的输出是结构化分析模型,它的组成结构如图3-1所示,其中包括数据流图(Data Flow Diagram, DFD)、实体-关系图(Entity-Relation Diagram, ERD)、状态转换图(State Transition Diagram, STD)和数据字典(Data Dictionary, DD),以及加工说明(Process SPECification, PSPEC)等。在20世纪80年代中期,以Ward和Hatley等为代表的学者在此基础上又扩充了行为模型,提出了控制流图(Control Flow Diagram, CFD)。
图3-1 结构化分析模型的组成结构
(1)分层数据流图建模
数据流图(DFD)的主要作用是指明系统中的数据是如何流动和变换的,并描述使数据流进行变换的功能。在DFD中出现的每个功能的描述则写在加工说明(PSPEC)中,它们一起构成软件的功能模型。DFD使用四种基本图形符号进行建模:①圆框代表加工;②箭头代表数据的流向,数据名称总是标在箭头的边上;③方框表示数据的源(发送数据)和宿(接收数据),即与系统打交道的人或外界系统;④双杠(或单杠)表示数据文件或数据库。图3-2给出了一个考务处理系统的0层DFD示例。
图3-2 DFD的示例
DFD采用“自顶向下,逐步求精”的方式进行分层建模,先画出顶层图,再画0层、1层……,直至足够详细。
(2)数据字典定义
最低一层DFD包含系统的全部数据和加工说明,从数据的终点开始,沿着DFD一步步向数据源点回溯,这样就容易看清楚数据流中每一个数据项的来龙去脉,然后定义它们的组成。表3-1中列出了数据字典使用的描述符号。
表3-1 数据字典使用的描述符号
采用上述符号,可以为DFD中的每个数据项逐一写出定义,并合并成数据字典。例如: 。
(3)加工说明定义
加工说明是对DFD中的每个加工所做的说明,由输入数据、加工逻辑和输出数据等部分组成。加工逻辑阐明把输入数据转换为输出数据的策略,是加工说明的主体。显然,在需求分析阶段,策略仅需要指出要加工“做什么”,而不是“怎样去做”。加工说明通常用结构化语言、判定表(decision table)或判定树(decision tree)、IPO图(Input-Process-Output chart)等来描述,如图3-3所示。
(4)实体-关系图建模
实体-关系图(ERD)用于描述数据对象间的关系,是软件的数据模型。在ERD中出现的每个数据对象的属性均可用数据对象说明来描述。ERD原来是描述数据库中各种数据之间的关系的图形表示工具,在数据库设计中早已广泛应用。由于这种图能直观、明了地表达数据间的复杂关系,所以在结构化分析中,尤其是某些包含复杂数据的应用,也将它用作数据分析和建模的工具。
在ERD中,实体(或数据对象)用长方形表示,实体之间的关联关系用线表示,这种关系还存在着与出现次数有关的对应值,称为基数(cardinality),如图3-4所示。
图3-3 判定表和判定树示例
图3-4 ERD的示例
(5)状态转换图建模
状态转换图(STD),简称状态图,用于指明系统在外部事件的作用下将会如何动作,表明了系统的各种状态以及各种状态间的变迁,从而构成行为模型的基础,关于软件控制方面的附加信息则包含在控制说明中。在STD中,用矩形表示状态,用线条表示状态的转变。图3-5是复印机的STD。
图3-5 STD的示例
结构化概要设计的任务就是把用DFD表示的分析模型转换为以结构图(Structure Chart, SC)表示的设计模型。在SC中,用矩形框来表示模块,用带箭头的连线表示模块间的调用关系,如图3-6所示。
图3-6 SC中模块调用的表示方法
DFD分为变换型结构和事务型结构两种。变换型结构的DFD由三个部分组成:输入、变换和输出,如图3-7所示。事务型结构的DFD图由至少一条输入路径、一个事务中心与若干条动作路径组成,如图3-8所示。
图3-7 变换型结构的DFD
图3-8 事务型结构的DFD
针对上述两种类型的DFD,结构化设计方法分别采用了两种映射方法——变换映射与事务映射,来把DFD方便地转换为初始SC,图3-9和图3-10是两种映射方法的示意图。
得到的SC需要进一步进行细化和改进。结构化设计方法提出了一系列优化软件结构设计的指导规则,即启发式设计策略。
1)降低模块的耦合度,提高内聚度。分割或合并SC中的模块,应该以提高模块独立性为首要标准:力求提高内聚度、降低耦合度,简化模块接口,以及少用全局型数据和控制型信息等。除此之外,也要适当考虑模块的大小。一般来说,模块的总行数应控制在10~100行的范围内,以便于阅读。
2)保持低扇出和高扇入。扇入(fan-in)高则上级模块多,能够增加模块的利用率;扇出(fan-out)低则表示下级模块少,可以减少模块调用和控制的复杂度。通常扇出数以3~4为宜,最好不超过5~7。如扇出过高,软件结构将呈煎饼形,此时可用增加中间层的方法使扇出降低。设计良好的软件通常具有瓮形(oval-shaped)结构,两头小、中间大,这类软件在下部收拢,表明它在低层模块中使用了较多高扇入的共享模块。
图3-9 变换映射示意图
3)模块的作用域应限制在该模块的控制域范围内。一个模块的控制域,是指该模块自身及其下级模块(即可供它调用的模块)。一个模块的作用域,是受这个模块中的判定所影响的模块。本规则要求,模块的作用域不要超出控制域的范围;软件系统的判定,其位置离受它控制的模块越近越好。
图3-10 事务映射示意图
结构化详细设计就是针对SC图中的每个模块给出足够详细的过程性描述,确定采用的算法、模块内数据结构和模块接口的细节,用某种选定的表达工具给出更清晰的描述。常用的结构化详细设计的表达工具包括:程序流程图(Flow Diagram)、PAD(Problem Analysis Diagram)、N-S图(N-S Chart)和伪代码等。以程序流程图为例,其图形符号如图3-11所示,具体示例如图3-12所示。
图3-11 程序流程图的图形符号
图3-12 “审定合格者”的程序流程图
面向对象方法(Object Oriented Method)是一种把面向对象的思想应用于软件开发过程,从而指导软件开发活动的系统方法,它建立在对象概念(对象、类和继承)的基础之上,是一种运用对象、类、封装、继承、多态和消息等概念来构造软件的开发方法。面向对象的思想最初起源于20世纪60年代中期的仿真程序设计语言Simula 67。20世纪80年代初出现的Smalltalk语言和20世纪90年代推出的C++、Java语言及其程序设计环境,先后成为面向对象技术发展的重要里程碑。从20世纪80年代末开始,面向对象方法得到快速发展,特别是20世纪90年代中期由Booch、Rumbaugh和Jacobson共同提出了统一建模语言(Unified Modeling Language, UML),把众多面向对象分析和设计方法综合成一种标准,自此,面向对象方法成为主流。
面向对象思想的最重要特征,是在解空间中引入了“对象”的概念,使之逼真地模拟问题空间中的客观实体,从而与人类的思维习惯相一致。面向对象方法包含以下核心概念:
1)对象(object)。对象是现实世界中个体或事物的抽象表示,是它的属性和相关操作的统一封装体。属性表示对象的性质,属性值规定了对象所有可能的状态。对象的操作是指该对象可以展现的外部服务。例如,若将卡车视为对象,则它具有位置、速度、颜色、容量等属性,对于该对象可施行启动、停车、加速、维修等操作,这些操作将或多或少地改变卡车的属性值(状态)。
2)类(class)。类用于表示某些对象的共同特征(属性和操作),对象是类的实例。例如,汽车类可包含位置、速度、颜色等属性,以及启动、停车、加速等操作。某一辆具体的卡车是汽车类的一个实例。
3)继承(inheritance)。类之间可以存在继承关系,它是现实世界中遗传关系的直接模拟,可用来表示类之间的内在联系以及对属性和操作的共享。子类可以沿用父类(被继承类)的某些特征,同时子类也可以具有自己独有的属性和操作。例如,飞行器、汽车和轮船都是交通工具类的子类,它们都可以继承交通工具类的某些属性和操作。
除继承关系外,现实世界中还大量存在着“部分-整体”关系。例如,飞机可由发动机、机身、机械控制系统、电子控制系统等构成。这种关系在面向对象方法中可表示为类之间的聚合(aggregation)关系。
4)消息(message)。消息传递是对象与其外部世界相互关联的唯一途径。对象可以向其他对象发送消息以请求服务,也可以响应其他对象传来的消息,完成自身固有的某些操作,从而服务于其他对象。例如,直升机可以响应轮船的海难急救信号,起飞、加速、飞赴出事地点并实施救援作业。
为了真正实现面向对象的优势,在应用面向对象技术进行分析与设计时,需要遵循四个基本原则——抽象、封装、模块化以及层次原则。
1)抽象(abstraction)是处理现实世界复杂性的最基本方式,抽象的结果反映出事物重要的、本质的和显著的特征。在面向对象方法中,它主要抽取事物的结构特征和行为特征,并组成一个有机的整体。抽象的结果有赖于特定的领域,具有客观性;抽象的结果有赖于特定的视角,具有主观性。在概念上,最核心的抽象内容是对象。准确地讲,对象是一个具有明确边界和唯一标识的、封装了行为和状态的实体。这里的实体是广义的概念,代表一个具有物理意义的实体,可以是一个纯粹软件意义上的实体,也可以是一个概念上的实体。
2)封装(encapsulation)将对象特征的实现方式隐藏在一个公共接口之后的黑盒中。封装概念的关键点在于被封装对象的消息接口。所有与该对象进行的沟通都要通过响应消息的操作来完成。除了对象本身,其他任何对象都不可能改变它的属性。封装在很多时候也被称作“信息隐藏”,信息有两个层面的含义,一方面是接口中操作的具体实施方法,另一方面是对象内部的状态信息。对于和某对象沟通的其他对象而言,只需了解它的消息接口即可顺利地与该对象进行沟通。封装起到两个方向的保护作用:一方面是对象内部的状态被保护起来,不会被与该对象沟通的其他对象直接篡改;另一方面是对象内部特征的变化不会改变其他对象与该对象的沟通方式。封装为面向对象系统带来一种叫作“多态”(polymorphism)的能力,即一个接口可以有多种实现。在概念上,接口是封装原则的准确描述手段,用于声明类或者构件能够提供的服务。
3)模块化(modularity)是关注点分离最常见的表现,软件被划分为独立命名、可处理的模块,把这些模块集成在一起可以满足问题的需求。模块化的关键点是通过分解技术来降低软件的复杂度,减少开发的成本。模块化的目标是将软件划分成一组高内聚(将逻辑上相关的抽象放在一起)、低耦合(减少模块间的依赖关系)的模块。在传统的结构化方法中,模块化主要是考虑对子程序进行有意义的分组;在面向对象方法中,模块主要表现为类、包和子系统。抽象、封装和模块化的原则是相辅相成的。一个对象围绕单一的抽象提供了一个明确的边界,封装和模块化都围绕这种抽象提供了屏障。
4)层次(hierarchy)的基本含义是不同级别的抽象组成一个树形的结构。层次的种类是多种多样的,可以是集合的层次、包含的层次、继承的层次,等等。简单地讲,层次就是一个描述分类的结构。层次的典型例子是生物中的门、纲、属、种、科等。层次的本质目的是表述并使用事物之间的相似性,同时事物之间的区别得以更加明显。这带来了两方面的好处:一方面,对同层次事物之间具有的相同特征,没有必要在这个层次内作分别的、重复的描述,可以将这部分内容放到更高的层次中去描述;另一方面,主体理解客体的概括程度是可选择的,主体可以根据实际需要决定采用较高层次的描述或者是较低层次的描述来认识客体。在概念上,层次的主要表现形式为类之间的泛化关系(generalization)。但是,层次的概念不仅仅局限于类之间的泛化,模式(pattern)乃至框架(framework)的复用在本质上同样是层次概念的应用。
根据UML 2.5版本,面向对象的模型分为结构模型和行为模型两类,如图3-13所示,一共14种图(泛化树上的叶结点) ,这14种图在面向对象分析与设计时并不一定都要用到,要视具体的软件来进行选择。
图3-13 UML图
面向对象的分析(Object Oriented Analysis, OOA)的目的是了解问题域所涉及的对象、对象间的关系和作用(即操作),然后构造问题的分析模型,力争该模型能真实地反映出所要解决的“实质问题”。在这一过程中,抽象是最本质、最重要的方法。在实际进行问题分析时,应当针对不同的问题性质选择不同的抽象层次,过简或过繁都会影响对问题的本质属性的了解和解决。
OOA强调直接针对问题域中客观存在的各种事物建立OOA模型中的对象,用对象的属性和服务分别描述事物的静态特征和行为。问题域有哪些值得考虑的事物,OOA模型中就有哪些对象,而且对象及其服务的命名都强调应与客观事物一致。另外,OOA模型也保留了问题域中事物之间的关系,例如把具有相同属性和相同服务的对象归结为一类,用一般/特殊结构(又称分类结构)描述一般类与特殊类之间的关系(即继承关系);用整体/部分结构(又称聚合结构)描述事物间的组成关系,等等。
可以看到,无论是对问题域中的单个事物,还是对各个事物之间的关系,OOA模型都保留着它们的原貌,没有转换、扭曲,也没有重新组合,所以OOA模型能够很好地映射问题域。OOA对问题域的观察、分析和认识是很直接的,对问题域的描述也是很直接的,它所采用的概念及术语与问题域中的事物保持了最大限度的一致,不存在语言上的鸿沟。
OOA方法的基本步骤包括:
1)采用用例技术对软件的功能进行用例建模;
2)识别出软件的关键抽象,即概念类,采用类图建立概念模型;
3)对每个用例进行用例分析,识别出分析类,建立类图和交互图。
OOA的更多内容和案例请参阅本书第4章。
在分析模型的基础上,进行面向对象的设计(Object Oriented Design, OOD),建立软件的设计模型。OOA与OOD的职责划分是:OOA针对问题域运用面向对象的方法,建立一个反映问题域的分析模型,不考虑与系统实现有关的因素(包括编程语言、图形用户界面、数据库等),从而使OOA模型独立于具体的实现;OOD则是针对系统的一个具体实现平台运用面向对象的方法。
OOD与OOA采用相同的表示法和模型结构,这是面向对象方法优于结构化方法的重要因素之一。从OOA到OOD不存在转换,只需局部的修改或调整,并增加与实现有关的独立部分。因此OOA与OOD之间不存在结构化方法中分析与设计之间的鸿沟,两者能够紧密衔接,大大降低了从OOA过渡到OOD的难度、工作量和出错率。
OOD方法的基本步骤包括:
1)从多个视图设计软件的架构,并选定设计模式。常见的架构视图包括逻辑视图、进程视图、开发视图、物理视图、用例视图、数据视图等;
2)在架构设计的基础上,确定子系统之间的接口,并对子系统内部进行设计;
3)进行详细的类的设计和优化。
OOD的更多内容和案例请参阅第5章。
基于构件的软件开发(Component Based Software Development, CBSD)方法 [1] 通过构件的构造和组装来开发一个新系统。构件是指独立于特定平台和应用系统,具有定义良好的接口,支持一定功能的、可复用与自包含的软件构成部分。在面向对象方法让人们能按客观世界规律进行软件分析和设计建模,使软件具有更好的可复用性和可维护性的同时,分布式计算的压力又给软件开发提出了许多挑战:它要求软件实现跨空间、跨时间、跨设备、跨用户的共享,这导致软件在规模、复杂度、功能上的极大增长,迫使软件要向异构协同工作、各层次上集成、可反复复用的工业化道路上前进。基于构件的软件开发方法正是为了适应这种需求而提出的。
从抽象程度来看,面向对象方法已达到了类级复用,它以类为封装的单位,这样的复用粒度还太小,不足以解决异构互操作和效率更高的复用。基于构件的软件开发方法将抽象提到一个更高的层次,通过构件对一组类的组合进行封装,代表完成一个或多个功能的特定服务,也为外界提供了多个接口。构件对外隐藏了具体的实现,只用接口提供服务。这样,在不同层次上,构件均可以将底层的多个逻辑组合成高层次上的粒度更大的新构件,甚至直接封装到一个系统,使模块的复用从代码级、对象级、架构级到系统级都可能实现,从而使软件像硬件一样装配定制而成的梦想得以实现。图3-14是基于构件的软件系统的一个示例,其中构件用长方形表示,箭头是构件提供和调用的接口。
图3-14 基于构件的软件系统示例
基于构件的软件开发方法具有以下优点:
1)提供了一种手段,使软件可用预先编好的、功能明确的构件定制而成,并可用不同版本的构件实现软件的扩展和更新。
2)利用模块化方法,将复杂的难以维护的系统分解为互相独立、协同工作的构件,并努力使这些构件可反复复用。
3)突破时间、空间及不同硬件设备的限制,利用统一的接口模型和标准实现跨平台的互操作。现有的一些构件标准与构件模型包括OMG CORBA、微软COM/DCOM、EJB/Java EE、OASIS SCA,等等。
与面向对象方法相比,基于构件的软件开发方法更加注重黑盒复用。通常面向对象开发者可以看到所使用的类的内部实现,也可以与开发伙伴协商改动。而对于使用者来说,构件的内部设计可能无法得到(也许因为构件是购买的),也不容易与开发者商量要求构件改动。这意味着基于构件的软件开发方法要求构件具有更正式的语义规约和接口的语法说明。
不同于“一切从零开始”的软件开发方法,基于构件的软件开发方法以领域软件架构为蓝图,以可复用的软件构件为组装基本单元,高效率、高质量地构造应用软件系统的方法。它主要包括三个阶段:构件开发、构件管理和构件组装,如图3-15所示。
1)构件开发。构件生产者基于领域工程方法对特定领域进行分析和设计,然后基于领域架构进行构件设计,开发符合构件模型规范的构件,或者把现有的构件包装成目标构件标准的构件。
2)构件管理。构件管理者把大量可复用的构件按照不同的特点(不同的刻面)进行管理,并让用户检索到符合用户需求的构件。
3)构件组装,即应用开发。在开发某个领域的应用系统时,不需要全部从头开始进行开发编码,可复用构件库(或构件市场)中的构件,当然也需要补充开发一些应用专用的构件,然后把这些构件组装起来成为一个完整的可运行的应用系统。这一阶段由应用开发者负责,包括构件之间组装关系的建模、构件组装模型的验证、构件组装黏合代码的开发,等等。
以下各小节将分别介绍这三个阶段。
构件开发可分为领域工程和构件工程,它首先进行领域开发,产生领域模型和领域基准体系结构,确定领域中潜在的可复用构件,然后进行构件的可变性分析,构建可复用构件。其具体步骤如下:
图3-15 基于构件的软件开发方法
1)领域分析和设计。采用领域工程的技术,对特定应用领域中已有的系统、预期的需求变化和技术演化进行分析,标识出整个领域中通用的需求,设计出领域的体系结构。首先收集领域中有代表性的应用样本,分析这些已有的系统和预期的需求变化中的公共部分或相似部分,抽取该领域的体系结构。这个领域基准的体系结构是可以裁剪和扩充的,以供该领域的应用复用。然后在此体系结构的基础上标识该领域的候选构件,对其进行泛化,提高其通用性。同时寻找候选构件在不同应用中可能修改的部分,即变化点(variation point),通过设置参数、继承或其他手段,使可变部分局部化。
2)构件设计和实现。采用构件工程的技术,设计和实现领域分析与设计产生的候选构件。首先以构件的接口为需求,对构件进行详细设计,然后编程实现,最后对构件进行严格的测试,以提高其可靠性。所使用的测试用例可跟随构件一起被复用。
所开发的构件应存放在构件库中,这需要相应的构件描述、分类和存储,以满足应用开发时对构件检索的要求。构件管理的任务包括组织、描述和分类构件,管理和维护构件库,评估构件资产。
1)组织、描述和分类构件,并将其放入构件库中进行管理,便于构件的检索,使构件使用者能够知道构件的存在,并很容易地找到需要的构件。
2)管理和维护构件库。建立和维护构件库,分配操作权限,对构件进行配置管理,包括建立基线、进行版本控制和变更控制,使构件使用者能够得到最新版本的构件。
3)评估构件资产,以不断改进构件。评估构件的价值包括构件的复用潜力、创建构件的成本、使用构件带来的利益、构件资产的应用情况、构件资产升级的频率、目前的构件资产分类是否合理等。
在基于构件的软件开发方法中,应用系统是由构件组成的,应用开发就是对构件进行组装的过程。首先进行应用系统分析,借鉴领域基准体系结构设计应用系统的体系结构,然后选择可复用构件进行组装,并开发少量构件胶合代码和应用特定代码,形成应用系统,最终通过测试。其具体步骤如下:
1)基于构件的设计。开发一个应用系统,首先要建立该应用的体系结构模型,将不同需求划分给软件架构中的不同构件,这是一种基于构件的体系结构,又称构件架构(component architecture)。基于构件的应用系统体系结构可采用“4+1”视图(即逻辑视图、进程视图、物理视图、开发视图和场景视图),只是对逻辑视图有特别的要求。其逻辑视图应描述组成应用系统的构件及其接口、构件之间的静态结构、交互、约束和关系。该模型可以使用领域工程提供的领域特定的基准体系结构,经裁剪或扩充而获得。
2)构件组装。这是一个构件选择、鉴定、特化和组装的过程。首先根据应用系统的体系结构,从构件库或其他可利用的构件源(如构件供应商)中寻找开发软件的候选构件。评价候选构件,以判断它是否适合待开发的软件。当有多个候选构件同时满足新软件的同一需求时,可根据复用代价、构件质量等加以选择。由于可复用构件具有通用性,因此在复用时有时需要对其进行特化,以满足特定应用的需求。如果构件中含有变化点,则要选择合适的变体(variant)链接到变化点上。一个新系统不可能全部由可复用构件组成,因此对新系统中未采用复用的部分仍需专门开发,最后将可复用构件和开发的代码组装成一个新的软件系统。
3)系统测试和发布。对组装后的软件系统进行集成测试和系统测试,最终将通过测试的系统进行发布。
在基于构件的软件开发活动结束后,应对所使用的可复用构件做出评价,提出修改或改进意见,以提高它以后的可复用性。同时还可根据新开发的部分,向构件库推荐可能的可复用构件,以不断扩充和完善构件库。
5G/6G移动通信、云计算、大数据和人工智能技术的蓬勃发展,使得人类社会进入了万物互联的时代。各种各样的应用系统都运行于基于Internet的开放分布计算环境中,通过Web协议实现动态交互协作,并可根据应用需求和网络环境变化而进行动态演化。在这种背景下,面向服务方法(Service Oriented Method)应运而生,它是以面向服务架构(Service Oriented Architecture, SOA)为基础的软件建模方法。基于面向服务架构的应用系统具备松散耦合、位置透明、协议独立的特征,可以充分复用现有的各种资源,并有效地实现随需应变。
SOA是一种构建分布式应用系统的架构技术,在SOA的世界中,服务占据了核心位置,围绕着服务包含三种角色,图3-16 [2] 显示了SOA中三种角色之间的协作。
图3-16 SOA中的角色及其协作
SOA中的角色包括:
● 服务消费者:即使用服务的应用系统、软件模块或其他的服务。它通过在服务注册中心中对服务进行查找,并通过指定的传输协议与服务绑定,来调用服务的功能。服务消费者是根据服务的接口契约执行服务功能的,例如根据WSDL(Web Service Description Language)文件中的约束来基于SOAP(Simple Object Access Protocol)的Web服务,或者按照REST(Representational State Transfer)协议来调用RESTful Web服务。
● 服务提供者:即在网络上可寻址的实体,它接受并处理来自服务消费者的请求。它将其服务和接口契约发布到服务注册中心中,以便服务消费者可以发现和访问其服务。例如,云提供商会以基础设施即服务(Infrastructure as a Service, IaaS)、存储即服务(Storage as a Service, StaaS)、平台即服务(Platform as a Service, PaaS)以及软件即服务(Software as a Service, SaaS)等多种形式将其资源通过URL开放给服务消费者使用。
● 服务注册中心:即服务发现使能者,它包含一个可用服务池,并允许服务消费者按照多个维度查找符合要求的服务提供者的位置和接口契约。服务注册中心还可以通过与服务访问网关集成,一方面,实现向服务消费者屏蔽服务的具体位置,以提高服务部署与调用的灵活性;另一方面,还可以实现服务多实例之间的负载均衡。
在SOA中,所有实体都可以扮演服务提供者、服务消费者和服务注册中心这三种角色中的一个或多个。它们能够执行的操作包括:
● 发布:服务总是希望能够被更多的消费者访问,因此,会将其服务描述发布到服务注册中心。例如,基于SOAP的Web服务会将其WSDL文件发布到服务注册中心。服务消费者可以通过在服务注册中心进行查询来发现与调用注册过的服务。
● 发现:服务消费者通过向服务注册中心查询,搜索满足查询条件的服务,获取这些服务的描述文件。
● 绑定与调用:在获取服务描述文件之后,服务消费者按照其中的服务接口信息来调用服务,包括传输协议、请求与响应的消息格式、服务用URL表示的地址等。
而在图3-16中涉及的软件制品包括:
● 服务:通过发布接口使其允许服务消费者进行调用。
● 服务描述:指定服务消费者与服务提供者交互的方式,指定服务请求和服务响应的格式,并且还可以指定前置条件、后置条件以及服务质量的级别等。
服务强调的是独立于具体的编程语言和实现,可以通过纯文本协议进行调用。因此,服务通常是对使用某种编程语言编写的构件或模块进行封装而得到的。例如,使用Java语言开发的服务器端构件,可以被Java语言开发的客户端通过远程方法调用(Remote Method Invocation, RMI)机制调用,也可以被C#语言开发的客户端按照REST协议来访问。所以说,服务并不是空中楼阁,它的实现仍然要基于面向对象和面向构件等开发技术。就像构件是在对象概念之上的更进一步封装一样,服务在逻辑概念上也位于构件之上,但是服务与构件之间并非一一对应的关系。这是因为在基于构件开发的系统中,构件是业务实体的映射,而在SOA系统中,服务是业务规则的映射。因此,服务与构件分别是在问题域和实现域的抽象,二者维度不同,所以绝对不能认为把构件进行包装,将其服务描述发布到服务注册中心,就可以完成基于构件的应用系统向SOA应用系统的迁移。
在SOA的应用中,系统由大量的服务构成,这些服务通常并不直接交互,而是通过企业服务总线(Enterprise Service Bus, ESB)以消息传递的方式实现与其他服务之间的交互,而传递的消息都是数据而非事件。因此,服务本质上是数据驱动的,由企业服务总线来转发各种消息。图3-17给出了ESB的概念模型以及SOA应用的构成。
正是由于企业服务总线在SOA应用中处于核心地位,因此,SOA应用会体现出三个主要特征:
● 松散耦合:服务和服务之间没有形成直接的调用,它们是数据驱动的,而调用消息以及响应消息则是由服务总线根据预定的业务流程来转发的。
● 位置透明:服务的位置对于服务的调用者来说并不重要,服务调用者只需要和服务总线交互即可,而服务总线起到了消息转发的功能。
● 协议独立:服务调用及响应消息可以使用各种传输协议来传输,而服务总线将通过各种适配器来实现协议的转换。
从上述描述可知,服务总线承载的功能使它成了重量级的基础软件,并且可能会成为服务之间交互性能的瓶颈。因此,很多SOA应用系统在实现时会根据实际需求有选择地使用服务总线,以避免出现系统性能瓶颈。
上面涵盖了SOA的主要概念。SOA实际上是内涵很广的一个技术架构,它使得分布式应用系统的设计与开发呈现出完全不同的面貌。
SOA解决方案由可复用的服务及服务总线等基础设施组成,这些服务带有定义良好且符合标准的已发布接口。这些接口独立于任何平台和语言,通常以纯文本的方式描述和调用,例如通过WSDL描述并通过SOAP调用。因此,SOA提供了一种将部署于不同的平台、使用不同的编程语言实现的应用系统以随需应变的方式集成在一起的机制。
图3-17 ESB的概念模型及SOA应用
从概念上讲,SOA中有三个主要的抽象级别 :
● 操作:代表单个逻辑内聚的工作单元的事务。执行操作通常会导致读、写、修改或删除若干个存储在持久性介质上的数据。SOA操作与面向对象编程中的方法(函数)类似,它们都有特定的结构化接口,并且返回结构化的响应。与方法一样,特定操作的执行可能涉及调用额外的操作。
● 服务:代表操作的逻辑分组。例如,如果将用户转账视为服务,则按照用户名查找客户、按照账户余额列出顾客和保存新的账户余额等就代表相关的操作。
● 业务流程:为实现特定的业务目标而执行的一组长期运行的动作或活动。业务流程通常包括多个服务调用,例如录用新员工、出售产品或服务。在处理订单流程中,每个流程都需要调用多个服务来实现其业务目标。在SOA术语中,业务流程包括依据一组业务规则按照有序序列执行的一系列对服务的调用。服务调用的排序、选择和执行称为服务编排或流程编排。
从建模的观点来看,由此带来的挑战是如何描述设计良好的操作、服务和流程的抽象特征,以及如何系统地构造它们。
面向服务的分析与设计(Service-Oriented Analysis and Design, SOAD)实际上是将面向对象的分析与设计(OOAD)、企业架构(Enterprise Architecture, EA)框架和业务流程建模(Business Process Modeling, BPM)等现有建模方法进行综合,并通过添加新的原理和规则而形成的,包括服务编排、服务库和服务总线中间件模式等在建模时需要给予特别关注的部分。
除了综合面向对象分析与设计、业务过程建模和企业架构框架技术之外,还有几个重要的方面需要特别关注:
● 服务分类和聚合:服务有不同的用法和用途,例如,可以从软件实现角度识别软件服务,也可以从业务逻辑角度识别业务服务,而这两者可以不是一对一映射的。对于复杂的业务逻辑,还可以将原子服务编排成级别更高、功能齐全的组合服务。服务组合可以通过可执行模型(如BPEL建模的模型)来加以简化。
● 策略和方面:服务具有语法、语义和服务质量特征,它们都必须进行建模;正式的接口契约必须涵盖比WSDL更多的内容。因此,Web服务策略(WS-Policy)框架是一个重要的相关规范。
● 中间汇合的流程:在真实世界中,并没有全新的项目,必须始终考虑遗留系统。因此,需要采用中间汇合的方法,而不是单纯的自顶向下或自底向上的流程。在设计取决于现有的IT环境而不是现在和将来的业务需要的情况下,单纯的自底向上方法往往会导致业务服务抽象效果不佳;而单纯的自顶向下方法可能会使得非功能性需求特征设计不充分,并且损害其他的架构质量因素,甚至还会在服务和构建之间产生不匹配的问题。
● 服务获取和知识代理:应该将复用看作识别和定义服务最主要的标准之一。如果构件(或服务)不可能复用,就无法将其作为服务进行部署,它可以依附于相关的服务,但是不能单独作为服务而存在。然而,即使从一开始就计划好了复用,还是需要将服务获取流程形式化。由众多使用者共同使用服务是SOA明确的设计目标之一,通过服务注册中心(例如企业UDDI目录)可以帮助解决部分问题,包括服务注册、查找和路由。
对应于面向服务建模所涉及的三个抽象层面,面向服务分析与设计也包含三个层面的内容:
1)业务层面:这个层面的设计又分为三个层面。首先是在功能域层面对业务逻辑进行描述,产生独立于计算的业务模型;其次是在业务流程层面使用标准的方式,对业务逻辑进行编排,产生规范的业务流程;最后是在业务服务层面对业务流程中的服务进行识别,从而产生业务服务集。构建业务流程可以使开发人员更加清晰地理解业务需求,并且由于很多开发工具都支持在构建业务流程之后、在不开发具体服务的情况下即可对业务流程本身进行逻辑测试,所以还可以通过对业务流程的测试来检验开发人员对需求理解的正确性。对业务服务的识别完全是从业务角度出发的,其原则是确保业务的边界和粒度划分都符合业务逻辑,因此它们和最终开发出来的软件服务之间可能并非一一映射关系。
2)服务层面:这个层面的设计是从软件设计和实现的角度出发,根据业务流程和业务服务集,设计对应的软件服务集。通常情况下,为了设计方便,业务服务集和软件服务集是一一映射关系,但是出于某些原因,例如为了对软件服务进行复用,业务服务集和软件服务集也可能并非一一映射。尽管如此,软件服务集必须实现业务服务集中的所有功能。软件服务集中的服务都是一些诸如Web服务这样的独立于具体的开发语言的服务。
3)构件层面:正如前面提到的,服务是对构件的包装而得到的,即服务最终是由构件实现的。构件是与开发语言相关的实体,需要在特定的开发语言和开发框架中进行设计。服务的粒度通常会比构件大,因此设计出来的构件数量通常也会进一步增多。同样出于复用的目的,开发人员可能会把在多个服务中共用的部分抽取出来构建成通用的工具构件,在多个软件服务中使用。
面向服务架构通常用来实现系统集成,即通过面向服务架构将多个互相关联但是独立开发的应用系统集成到一起。因此,在进行以系统集成为目的的面向服务的设计时,会发现构件层面的内容已经全部或大部分都具备了,开发人员要做的是从底层的构件层面向上,并从顶层的业务层面向下,最终在软件服务和业务服务的中间点汇合,从而得到完整的设计方案。这种设计流程可以简略地描述如下:
● 自顶向下分解:在进行业务层面的设计时,根据用户规格说明书,在功能域进行业务分解,将系统分解成若干子系统或模块,并创建在这些子系统中的业务流程。在进行业务服务识别时,可以以业务分解得到的用例为依据,将业务服务的粒度和边界控制在合理的范围内。
● 自底向上抽象:在进行构件层面的设计时,要从现有的系统出发,通过分析选择合适的构件包装成软件服务,以实现对业务流程中识别出来的业务服务的支持。在进行构件选择和服务包装时,需要考虑的因素包括原有接口、暴露边界和安全性等。在必要的时候,还会对现有系统进行重构,以使其构件适合被包装成软件服务。
● 中间汇合:在这里需要将软件服务和业务服务进行映射,由于是从两头出发并行设计的,因此这种映射很可能并非一一映射。除了映射之外,还需要考虑质量属性方面的需求,例如系统性能和可扩展性等。
以UML的广泛接受和使用为基础,OMG在2001年提出模型驱动架构(Model Driven Architecture, MDA),使模型成为系统的实现,而不再仅仅作为文档出现。这标志着模型驱动开发(Model Driven Development, MDD)方法广为接受,并开始在实践中推广。模型驱动开发提高了软件开发行为的抽象级别,倡导将业务逻辑定义为精确的高层抽象模型,让开发人员从烦琐、重复的低级劳动中解脱出来,更多地关注业务逻辑层面,从而提高软件开发效率、软件可复用性和可维护性。纵观软件产业几十年的发展,我们一直致力于在底层硬件上叠加更高的抽象层次,从汇编语言到高级语言,从代码段的复用到类库和框架的复用,从项目特定代码到设计模式。现在,模型驱动开发方法则把软件开发的核心从代码转移到了模型。
模型驱动开发方法就是对实际问题进行建模,并转换、精化模型直至生成可执行代码的过程。在模型驱动开发中,模型不再仅仅是描绘系统、辅助沟通的工具,而是软件开发的核心和主干。模型之间通过模型映射机制相互转换,保证了模型的可追溯性。软件的开发和更新过程就是模型自顶而下、逐步精化的过程。模型驱动开发的基本思想是:一切都是模型。软件的生命周期就是以模型为载体并由模型转换来驱动的过程。
模型驱动开发方法的通常流程如图3-18所示,开发者对业务需求进行分析和抽象,采用某种建模语言(如UML)建立平台无关模型(PIM);采用Model-to-Model模型转换技术将PIM转换成平台相关模型(PSM),需要时对PSM进行精化和优化;再采用Model-to-Text模型转换技术根据PSM生成代码。在实践中,可根据需要对该流程进行裁剪。例如,PIM至PSM的转换采用人工完成;PSM至代码只生成代码框架或部分代码,再和人工编写的代码进行集成,得到最终的软件系统,等等。又如嵌入式系统领域的SCADE工具,开发者定义PSM后,工具就能进行自动模型检验,并生成C语言代码。
图3-18 模型驱动开发方法的通常流程
另一种模型驱动开发方法是基于领域特定语言(Domain Specific Language, DSL)的开发。针对特定领域,进行领域分析,定义新的元模型(如对UML元模型进行扩充),设计出领域特定的建模语言。开发者采用DSL进行领域应用的编程,所编写的DSL程序通过解释器直接解释执行,或者通过生成器生成C等通用语言的代码来编译执行。例如,物联网领域的IFTTT平台,开发者用IFTTT语言编写物联网代码(即If-Then规则)后,平台就能解释执行。
以下几小节将从方法的核心——模型和元模型的概念出发,阐述两个关键技术——模型转换和DSL。
软件模型是对软件系统的抽象定义,采用建模语言进行刻画,如通用建模语言(UML)和领域特定的建模语言(SCADE)。元模型(meta-model)就是定义和扩展建模语言的模型,它是模型的模型。所有的建模语言都有其特定的元模型定义,即开发者采用某种建模语言所设计的模型必须遵循它的元模型所定的语法和语义。
为了描述、构造和管理元模型,OMG提出了元对象设施(Meta Object Facility,MOF) [3] 的概念。MOF是一种元模型的模型,即元元模型(meta-meta-model)。于是,模型实例-模型-元模型-元元模型就构成了四层元模型体系结构,如图3-19所示。
图3-19 MOF的四层结构
在图3-19中,M0是模型实例层,包含对象和数据,表示的是系统中的实体,是系统所运行的层次。M1是模型层,包含各类模型,是系统的模型,M0层的元素都是M1层的实例。M2是元模型层,比如UML、CWM等已经被标准化的元模型,M2层的元模型定义了M1层中的模型。M3是元元模型层,是用来定义如UML语言、IDL语言用的模型,M3层是自描述的。每个较低的层次都是其上一个层次的实例,都被其上的层次所定义,只有到了第M3层时,它才是自定义的。简单地说,MOF就是用来定义元模型的结构集。我们可以按照这个思路不断地增加层次,但是实践表明,更多的层次也没有什么意义。元层次本质上不限于四层,重要的是每层之间的实例化关系,以及M3层的自描述能力。
除了采用现有的建模语言进行模型的建模,我们还可以根据特定领域需要,采用MOF定义该领域的元模型,设计出特定领域的建模语言或DSL,也可以对现有建模语言的元模型进行扩展(如UML提供了Profile机制),增强该建模语言的建模能力。
模型转换是模型驱动开发的关键操作,用于实现不同模型之间(例如PIM-PSM)、模型和文本(例如PSM-代码)之间的转换。模型转换可分为两类:
1)模型之间(Model-Model),例如PIM-PIM、PIM-PSM和PSM-PSM等。MOF提出了QVT语言来定义模型之间的转换规则。
2)模型和文本之间(Model-Text),例如从PSM生成代码、部署脚本、文档、报告等。MOF提出了MOFM2T语言来定义模型和文本之间的转换规则。
如何实现模型转换?目前普遍采用基于模板的方法来实现一组转换规则集。我们可以运用通用的编程语言(如Java)编写转换规则,但这降低了转换规则的抽象层次,增加了开发的复杂度。因此,主流的方法是利用模型转换语言(如QVT、AGG、TGG、ATL等)编写转换规则。为便于针对建模语言用模型转换语言构造转换规则,源模型建模语言、目标模型建模语言和模型转换语言应该是由同一元建模语言建立的。
图3-20展示了元建模、建模、模型间转换之间的关系。元建模者在元建模工具的支持下,使用元建模语言建立建模语言和模型转换语言。建模者在建模工具的支持下,用建模语言建立或完善模型。在模型转换工具的支持下,转换规则开发者针对建模语言用模型转换语言建立转换规则。转换工具基于转换规则把一种模型(源模型)转换为另一种模型(目标模型),如把PIM转换为PSM。模型和文本之间的转换也同理。
图3-20 不同模型间的转换
领域特定语言(Domain Specific Language, DSL)是描述特定领域或问题的有效手段,它通过适当的表示方法和抽象机制来提供对特定问题领域的表述能力,因此,DSL常用来生成特定应用领域中的一个产品/应用家族的各成员。
DSL既可以是用文本表达的,也可以是用图形表达的(具体词法)。这一具体词法可以映射到一个语言模型(抽象词法),它描述了模型该如何在一个领域中用相关的建模元素来创建。模型的语义通常由模型转换和代码生成来定义。因而,DSL本身就主要是由元模型来定义的。创建一个模型驱动的DSL一般需要以下三步:
1)定义一个语言模型来形式化地描述DSL的抽象词法。一般地,语言模型是从一个形式化的元模型得来的。例如采用UML的各种扩展机制来定义DSL。
2)定义一个恰当的具体词法。因为具体词法表达的是抽象词法中的概念,所以在具体词法和抽象词法元素之间存在着一个对应的关系。
3)利用一个生成器将DSL翻译为一种可执行的表达。要做到这一步,必须按照DSL的形式化语言模型把具体词法元素映射到抽象词法的实例上。目标编程语言的代码是从抽象词法生成的,在这两者之间可能会有多种不同的模型转换步骤,这可以通过利用各种模型转换和代码生成技术来实现。在实际操作中,生成器必须能够定义DSL的语义。
传统的DSL一般都以一种特定的方式定义,没有系统的标准,属于“特事特办”型,而在模型驱动的DSL方法中定义DSL时不但引入了系统性过程,同时还对生成器的基础构造和先前定义过的元模型也进行了复用。最终,这种方法提供了一个清晰的概念来把面向对象模型和DSL的概念结合起来。
对DSL的研究和实践已有很长的历史,从早期UNIX系统的Shell语言、数据库的查询语言SQL到编译生成程序YACC,以及专用的排版系统LaTex,这些都是DSL的成功范例。在工具支持下,DSL定义的领域模型可直接生成代码,或解释执行。
软件复用和构件技术作为提高软件开发的效率与质量的重要途径已经得到广泛的认同,软件产品线(Software Product Line)工程 [4] 则将复用从单个产品提升至产品簇。它针对特定领域中的一系列具有公共特性的软件系统,通过对领域共性(commonality)和可变性(variability)的把握构造一系列领域核心资产,使特定的软件产品可以在这些核心资产基础上按照预定义的方式快速、高效地构造出来。
软件复用对任何一种软件开发方法都是有效的,如结构化方法中的函数库、面向对象方法中的UML设计模式和类库、以代码复用为核心的基于构件的开发方法,都能明显提高软件开发的效率和质量。经过对软件复用的大量研究和实践,人们发现,特定领域的软件复用活动相对容易取得成功,这是由领域的内聚性和稳定性决定的。在此背景下,CMU/SEI借鉴制造业中生产线的成功经验,提出了软件产品线概念,其特点在于维护公共软件资产库,并在软件产品开发过程中复用这些资产。
软件产品线是指“共享一组公共受控特征,满足特定市场需要,并且按照预定方式在相关核心资产基础上开发而成的一系列软件系统”。一条产品线是共享一组共同设计及标准的产品族,从市场角度看是在某市场片段中的一组相似的产品。这些产品属于同一领域,具有公共需求集,可以根据特定的用户需求对产品线体系结构进行定制,在此基础上通过通用构件和特定应用构件的组装得到。在此构件是指包括软件服务在内的广义的构件。产品线工程是一种有效的、系统的软件复用形式,复用的对象包括产品线体系结构、构件、过程模型等。软件产品线在产业界已进行了不少实践,并取得了良好的收益。
软件产品线工程由三个基本活动组成——领域工程、应用工程和产品线管理,如图3-21所示。
1)领域工程,是产品线核心资产的开发阶段,属生产者复用。它定义和实现了产品线的共性和可变性。所谓共性是指产品线中多个应用完全相同的那一部分特性;可变性则将不同应用区分开来。领域工程通过领域分析识别出产品线的共性和可变性,在此基础上设计、开发和完善可复用资产,包括领域的产品线软件体系结构和可复用构件等。
2)应用工程,是基于核心资产的应用产品开发阶段,属消费者复用。它根据单个软件产品的特定需求对领域模型进行定制,通过对领域资产的复用,使用产品线的可变性实现目标产品。产品线的最终价值体现在应用工程具体软件产品的开发过程中。
3)产品线管理,包括技术和组织管理两个方面。成功的产品线需要持久的、强有力的、有远见的管理,这是由于软件产品线涉及领域工程和应用工程两个层面,而应用工程中又包含多个并可能随时间推移不断增加的应用产品。此外,软件产品线往往是一个长期的投资过程,需要经历领域发展、成熟以及核心资产不断积累的过程。因此,在长期的软件产品线开发和不断演化过程中,如何管理各种开发活动、协调核心资产与应用产品两个层面上的开发和演化就显得十分重要了。
三个基本活动紧密联系,各自不断更新,同时引发其他活动的更新,更新可以以任何次序出现且反复循环。正向的开发活动通过核心资产开发用于应用开发,逆向的开发活动则从现有产品中挖掘公共资产放入产品线资产库。核心资产开发和产品开发之间存在很强的反馈循环,即使采用正向开发,核心资产也可能随着新的产品开发而更新。由于领域总是处于不断的发展变化中,而开发者对领域预见性的把握并不总是准确的,因此需要持久的、强有力的、有远见的管理。
图3-21 软件产品线工程的三个基本活动
显式定义和管理可变性是软件产品线工程的两个重要特性之一,它是产品线工程与单一系统工程中软件复用的重要区别。软件产品线的可变性通过变化点(variation point)来定义,变化点既代表了变化主题,又包含了上下文信息。变化点主要有两个来源:一是不同客户的不同需求,例如有的汽车制造公司希望汽车巡航系统提供多国语言的支持;二是软件工程师需要使系统支持或使用不同的技术,例如汽车巡航系统支持雷达和激光两种测距方法。
可变性建模方法主要可以分为两类:一类是把可变性作为传统软件模型的组成部分,例如把可变性集成到用例模型、特征模型、时序图和类图等;另一类是把传统软件模型和可变性模型分离开来,例如图3-22的正交变化模型。这两种方法各有优缺点,前者简单易行,但由于在传统模型中增加了可变性信息,使模型变得复杂,并且难以实现软件可变性的全局描述;后者则在单独的模型中定义可变性,实现了可变性的全局描述,但需要新增模型。
图3-22 正交变化模型的示例
我们通过对变化点的定义来对可变性进行建模,其内容至少应包含:
1)变化点的表示。
2)变化点的可变体。
3)变化点与可变体之间的变化依赖关系,基本关系有:可选性(optional)、选择性(alternative)、多选性(or)。可选性表示变化点上只有一种可变体,并且需要确定是否被选中;选择性表示变化点上有多种可变体,且只有一种可以被选中;多选性表示可以从多种可变体中选中多个。
4)变化点之间的约束依赖,基本关系有:需要和排斥。
5)可变体之间的约束依赖,基本关系有:需要和排斥。
6)可变性模型与其他开发模型之间的追踪关系。
在产品线工程的基本活动中,核心资产的开发,即领域工程,是决定因素,也是难度最大的环节。领域工程通过领域分析、领域设计、领域实现三个阶段,将一个领域的知识转化成为一组规约、架构和相应的可复用构件。
(1)领域分析
领域分析负责确定领域范围,分析领域共性和可变性需求,并将结果用易于理解的方式表示出来,形成领域模型。
首先确定产品线的领域范围,这受到成本、复用价值和易复用性等多种因素的影响。成本制约要求领域的范围应该保持在一个合理的尺度内,即领域不可能无限制地覆盖所有共性和可变性。同时,为了提高领域工程输出的可复用资产的复用价值,领域应尽可能多地包含共性成分;而为了提高可复用资产的易复用性,领域又必须尽可能考虑如何使这些共性成分更好地适应复用场景中存在的各种可变性。因此,如何在投资预算的基础上综合考虑产出的可复用资产的复用价值和易复用性,从而在共性和可变性之间寻求一个适当的折中点,就成为领域范围确定中的关键问题。
然后识别和分析软件产品线上的一组具有相似需求的应用系统的共性和可变性需求。表3-2是采用应用系统-需求矩阵进行分析的示例,矩阵的左列是所有应用中的需求,首行表示产品线中的应用系统。在矩阵中表示需求对哪个应用系统是强制有的。
从应用系统-需求矩阵中可以很方便地看出哪些需求是所有应用系统所必需的,哪些需求是一些应用系统共有的,而哪些需求是个别系统需要的。将所有应用系统所必需的需求确定为共性需求,例如定速巡航和监控引擎;将其他需求定为可变性需求,例如自动减速和测距。
表3-2 应用系统-需求矩阵示例
共性需求的建模方法和常规软件需求建模一样,可以采用用例图、特征模型、类图、状态机图等来刻画。可变性需求则应当通过变化点、可变体及其关系来定义。
(2)领域设计
领域设计将软件开发人员在开发同一领域中的系统时逐渐积累起来的经验显式地表达出来,以便在未来的开发中复用。领域设计包含两个主要活动:一是依据领域模型设计出领域中的应用系统所具有的共性软件体系结构,即特定领域的软件体系结构(Domain Specific Software Architecture, DSSA);二是对领域模型中的可变性需求进行设计。领域设计的成果是进一步识别和生产源代码级可复用资产的基础。
从领域需求设计出相应的DSSA在本质上是一个从需求到设计的转化过程,涉及问题域部分、人机交互部分、控制接口部分和数据接口部分。这四个部分的设计在时间上没有一定的先后关系,可以根据实际情况穿插进行。DSSA设计有两种方法:其一是在现有系统的体系结构模型中选择一个作为基础,依据领域需求,并参考其他系统的体系结构,对这个模型进行补充和修改;其二是依据领域需求,并参考现有系统的体系结构,逐步建立一个新的体系结构模型。
然后在DSSA下进行可变性设计,常用技术包括(但不限于):
● 构件框架。框架由构件和接口组成,我们可以通过接口把可变性封装起来,不同的构件实现变化点的不同的可变体。
● 特定应用插件。针对不同的应用系统,插入不同的插件,实现不同的可变性需求。
● 设计模式。设计模式为如何针对不同的问题分离可变部分提供了解决方案。
● 配置。采用产品配置的方式来定制可变性需求,例如通过文件或数据库进行配置,或开发专门的配置工具或配置语言。
(3)领域实现
领域实现负责在领域分析和领域设计的基础上,实现领域中的DSSA和构件等可复用资产。常用的实现方法包括:依据领域设计的模型重新实现DSSA和构件;从现有应用系统中利用再工程技术提取DSSA和构件;对现有构件进行重新包装;通过购买等方式获取外界的DSSA或构件等。
开发可复用构件是从设计到代码的转换,必须遵循DSSA,且应尽量减少构件与外部的依赖关系,建立良好的接口规约,为每个接口提供简单的文本描述、类型规约、参数的取值范围和对越界参数的处理方法等信息。同时应为复用者进行效率调优提供机会,帮助复用者选择正确的语言成分和恰当的环境设置。
为了提高软件开发的质量和效率,人们提出了多种解决方法,归纳起来有两类:一是采用工程方法来组织、管理软件的开发过程;二是深入探讨程序和程序开发过程的规律,建立严密的理论,以期用来指导软件开发实践。前者会导致软件工程中工程准则的发展,后者则会推动对形式化方法的深入研究。所谓形式化方法(Formal Method) ,是指采用数学(逻辑)证明的手段对计算机系统进行建模、规约、分析、推理和验证的方法。它是保证计算机系统正确性与安全性的一种重要方法,目前在安全攸关的软件系统(如航空航天软件和医疗设备软件)中取得了令人瞩目的成果。形式化方法已融入软件开发过程的各个阶段,从需求分析、架构设计、算法设计、编程、测试直至维护。
形式化方法是利用数学描述、验证软件或硬件及其性质的技术。数学为软件开发提供了精确定义、一致性、完整性等概念,并进一步提供了定义规约、实现和正确性的机制。不同形式化方法的数学基础是不同的,有的以集合论和一阶谓词演算为基础(如Z和VDM),有的以逻辑、状态机、网络、进程代数、代数等为基础。形式化方法与非形式化方法的本质区别是显而易见的:形式化方法的基础是数学意义上的准确定义;非形式化的软件开发方法并不具备数学基础,因而缺乏精确性。
形式化方法主要包含三部分内容:
1)形式化规约。形式化规约提供了目标系统及其性质的精确描述,从而帮助软件开发人员准确理解目标系统及其相关性质,精确开发软件,以及严格验证软件。形式化规约需要形式化规约语言的支持。
2)形式化开发。基于软件的形式化规约,开发人员可以严格开发软件,例如,要求开发的代码的行为与规约一致,根据规约生成代码或定义代码的断言。
3)形式化验证。这主要是指通过完全的自动证明来协助软件、硬件的开发和验证,即自动证明软件、硬件的形式化规约的正确性,证明其满足可靠性及安全性需求等。一般来说,理论证明最难,代价也最为昂贵,它常常被用在那些高可靠、高安全的系统(如微处理器设计中的关键部分)中,证明目标软件满足某些性质等。
形式化方法可以被用来描述被开发的软件及其性质,所得到的文档通常被称为形式化规约。形式化规约是对程序需要“做什么”的数学描述,是用具有精确语义的形式语言书写的程序功能描述,它是设计和编制程序的出发点,也是验证程序是否正确的依据。
形式化规约的方法主要可分为两类:一类是面向模型的方法,也称为系统建模,这些方法通过构造目标系统的计算模型来刻画系统的不同行为特征;另一类是面向性质的方法,也称为性质描述,该方法通过定义系统必须满足的一些性质(例如功能、实时性、性能、系统内部结构等要求)来描述目标系统。
描述形式化规约的语言通常被称为形式化规约语言。不同的形式化规约方法要求不同的形式化规约语言,包括代数语言(如OBJ、Clear、ASL、ACT One/Two等)、进程代数语言(如CSP、CCS、π演算等)和时序逻辑语言(如PLTL、CTL、XYZ/E、UNITY、TLA等)。这些规约语言基于不同的数学理论及规约方法,因而也千差万别,但它们有一个共同的特点,即每种规约语言均由基本成分和构造成分两部分构成,前者用来描述基本(原子)规约,后者把基本规约组合成复杂规约。
形式化方法可以应用于软件过程的任意或所有阶段。最好的应用策略是在软件过程的早期各阶段运用形式化方法,并选择性地将形式化方法添加到后期阶段。
在需求阶段,非形式化的软件需求是通过自然语言进行描述的,很难进行(手工或者工具)检查。引入形式化方法,并不改变自然语言所描述的需求,但是形式化方法可以用于软件需求规约、证明,从而增强软件需求的严谨性。此外,形式化的需求规约有助于确保最终软件的验证性。在设计阶段,采用形式化方法进行PSM建模,不仅能通过形式化分析和仿真等避免其他软件开发方法所引入的歧义性、不完整性和不一致性问题,而且还能在建模工具的支持下从形式化PSM自动生成代码和测试用例。在编码阶段,除了自动生成代码外,如果形式化规约的语义是前置条件与后置条件,则前置条件与后置条件将成为代码的断言。在测试阶段,可采用设计阶段生成的测试用例进行测试,还能用形式化验证来代替一部分测试。如果形式化规约的语义是操作语义,则应验证软件的行为与该规约所描述的行为是否一致。
形式化开发的优点是产生正确的软件。不过,在工业界实际使用时会遇到以下问题:形式化方法比较费时和昂贵,需要强有力的工具支持;因为很少有软件开发者具有使用形式化方法所需的背景知识,所以尚需多方面的培训;难以用软件的形式化规约与对其一无所知的用户进行沟通等。
一旦形式化规约已经制定,该规约可以作为依据,证明有关的规约性质。证明手段主要包括手工证明、定理证明和模型检验三种。
1)手工证明。有时,对系统进行证明并不是为了保证其正确性,而是希望更好地了解系统。因此,一些软件的正确性证明具有典型的数学证明风格:证明过程使用自然语言,也并不严格。一个良好的证明应当具有可读性,容易被其他人所理解。需要注意的是,软件中可能存在一些细微的错误,而自然语言存在模糊性,常常导致手工证明过程中不能发现这些细微错误。此外,良好的证明过程需要具有复杂的数学和专业知识。
2)定理证明。通过提供目标软件、逻辑公理和一系列推理规则形成一个逐步证明的方案。定理证明由工具自动进行证明,但需要人工去发现哪些性质是应当被证明的。
3)模型检验。利用有限状态机来自动验证系统的正确性。通过穷尽搜索目标系统在执行时的所有状态,以验证系统符合某些性质。模型检验由工具自动执行,但需要人工提供一个抽象的模型,否则复杂系统的状态将非常多,形成空间爆炸。
在业界中,定理证明或模型检验更多地被集中使用在集成电路或者嵌入式系统的设计和验证中。例如,此前,由于奔腾芯片的FDIV错误,通常需要对处理器的浮点计算提供额外的审阅工作;通过定理证明,AMD、英特尔等芯片厂商可以确保其生产的处理器上的除法和其他操作的实现是正确的。
1.什么是模型?软件模型的三个层次分别是什么?请对每个层次进行简要说明。
2.结构化分析模型和设计模型分别由哪些模型组成?
3.请列出面向对象的基本原则。
4.什么是构件和基于构件的软件开发方法?
5.面向服务架构中松散耦合、位置透明和协议独立这三个特性是如何实现的?
6.什么是元模型?请简要介绍模型的MOF四层结构。
7.什么是软件产品线工程?它的三个基本活动分别是什么?
8.什么是形式化方法?请分析它的优点和缺点。
[1] 参见Umesh Kumar Tiwari和Santosh Kumar著 Component-Based Software Engineering : Methods and Metrics 。
[2] 参见Endrei Mark、Ang Jenny和Arsanjani Ali等著 Patterns : Service-Oriented Architecture and Web Services 。
[3] 参见Marco Brambilla、Jordi Cabot和Manuel Wimmer所著的 Model-Driven Software Engineering in Practice , 2nd Edition 。
[4] 参见Markus Roggenbach和Antonio Cerone等著 Formal Methods for Software Engineering : Languages , Methods , Application Domains 。