第1章面向对象分析与设计引论
自20世纪40年代问世以来,计算机在人类社会的各个领域得到了广泛的应用。20 世纪60年代以前,计算机刚刚投入实际使用,软件设计往往只是为了一个特定的应用而在指定的计算机上设计和编制,软件的规模比较小,文档资料通常也不存在,很少使用系统化的开发方法,设计软件往往等同于编制程序,基本上是个人设计、个人使用、个人操作、自给自足的私人化的软件生产方式。20 世纪60年代中期,随着计算机性能的提高,计算机的应用范围迅速扩大,软件开发的需求也急剧增长。软件系统的规模越来越大,复杂程度越来越高,软件可靠性问题也越来越突出。原来的个人设计、个人使用的方式不再满足企业和市场要求,改变软件生产方式,提高软件生产率变得尤为迫切。
为了解决长期以来计算机软件开发的低效率问题,计算机业界提出了软件工程的思想和方法。面向对象技术是一种系统开发方法,是软件工程学的一个重要分支。在面向对象编程中,数据被封装(或绑定)到使用它们的函数中,形成一个整体称为对象,对象之间通过消息相互联系。面向对象建模与设计是使用现实世界的概念模型来思考问题的一种方法。对于理解问题、与应用领域专家交流、企业级应用建模、编写文档、设计程序和数据库来说,面向对象模型都非常有用。
1.1软件系统概述
1.1.1 软件的概念和特点
计算机是一个复杂的设备,它通常包含的要素有:计算机硬件、计算机软件、人员、数据库、文档和过程。其中,硬件是提供计算能力的电子设备;软件是程序、数据和相关文档的集合,用于实现所需要的逻辑方法、过程或控制;人员是硬件和软件的用户和操作者;数据库是通过软件访问的大型的、有组织的信息集合;文档是描述系统使用方法的手册、表格、图形及其他描述性信息;过程是一系列步骤,它们定义了每个系统元素的特定使用方法或系统驻留的过程性语境。
计算机软件是指由系统软件、支撑软件和应用软件组成的软件系统。系统软件用于管理计算机的资源和控制程序的运行,主要功能是调度、监控和维护计算机系统,管理计算机系统中各种独立的硬件,使得它们可以协调工作。系统软件使得计算机使用者和其他软件将计算机当作一个整体而不需要顾及底层每个硬件是如何工作的(如Windows、Linux、DOS、UNIX等操作系统都属于系统软件)。支撑软件包括语言处理系统、编译程序,以及数据库管理系统等。应用软件是用户可以使用的,由各种程序设计语言编制的应用程序的集合。应用软件是为满足用户不同领域、不同问题的应用需求而提供的,它可以拓宽计算机系统的应用领域,为人们的日常生活、娱乐、工作和学习提供各种帮助(如Word、Excel、QQ等都属于应用软件)。
计算机软件是用户与硬件之间的接口界面。用户主要是通过软件与计算机进行交流。如果把计算机比喻为一个人,那么硬件就表示人的身躯,而软件则表示人的思想、灵魂。一台没有安装任何软件的计算机我们把它称为“裸机”。
软件是计算机系统设计的重要依据。为了方便用户,为了使计算机系统具有较高的总体效用,在设计系统时,必须通盘考虑软件与硬件的结合,以及用户的要求和软件的要求。软件的正确含义应该是:①运行时,能够提供所要求功能和性能的指令或计算机程序集合;②程序能够满意地处理信息的数据结构;③描述程序功能需求以及程序如何操作和使用所要求的文档。
软件具有与硬件不同的特点:①表现形式不同。硬件有形,看得见,摸得着。而软件无形,看不见,摸不着。软件大多存在人们的头脑里或纸面上,它的正确与否,是好是坏,一直要到程序在机器上运行才能知道。这就给设计、生产和管理带来许多困难。②生产方式不同。硬件是设备制造,而软件是设计开发的,是人的智力的高度发挥,不是传统意义上的硬生产制造。虽然软件开发和硬件制造存在某些相似点,但二者有根本的不同:两者均可通过优秀的设计获得高品质产品,然而硬件在制造阶段可能会引入质量问题,这在软件设计开发中并不存在(或者易于纠正);二者都依赖人,但是人员和工作成果之间的对应关系是完全不同的;它们都需要构建产品,但是构建方法不同。软件产品成本主要在于开发设计,因此不能像管理制造项目那样管理软件开发项目。③要求不同。硬件产品允许有误差,而软件产品却不允许有误差。④维护不同。硬件是要用旧用坏的,在理论上,软件是不会用旧用坏的,但在实际上,软件也会变旧变坏。因为在软件的整个生存期中,一直处于改变维护状态。
1.1.2软件的本质
现在的软件技术具有产品和产品交付载体的双重作用。作为一个产品,它显示了由计算机硬件体现的计算能力,更广泛地说,显示的是由一个可被本地硬件设备访问的计算机网络体现的计算潜力。无论是驻留在移动电话还是在大型计算机中,软件都扮演着信息转换的角色:产生、管理、获取、修改、显示或者传输各种不同的信息,简单如几个比特的传递或复杂如从多个独立的数据源获取的多媒体演示。而作为产品交付载体,软件提供了计算机控制(操作系统)、信息通信(网络)以及应用程序开发和控制(软件工具和环境)的基础平台。
软件提供了我们这个时代最重要的产品—信息。它会转换个人数据(例如个人财务交易),使信息在一定范围内发挥更大的作用;它通过管理商业信息提升竞争力;它为世界范围的信息网络提供通路(比如因特网),并对各类格式的信息提供不同的查询方式。
在最近半个世纪里,计算机软件的作用发生了很大的变化。硬件性能的极大提高、计算机结构的巨大变化、内存和存储容量的大幅度增加、还有种类繁多的输入和输出方法都促使计算机系统的结构变得更加复杂,功能更加强大。如果系统开发成功,复杂的结构和功能可以产生惊人的效果,但是同时复杂性也给系统开发人员带来巨大的挑战。
现在,一个庞大的软件产业已经成为工业经济中的主导因素。早期的独立程序员也已经被专业的软件开发团队所代替,团队中的不同专业技术人员可分别关注复杂应用系统中的某一个技术部分。然而同过去独立程序员一样,在开发现代计算机系统时,软件开发人员依然面临同样的问题:
1)为什么软件需要如此长的开发时间?
2)为什么开发成本居高不下?
3)为什么在将软件交付顾客使用之前,我们无法找到所有的错误?
4)为什么维护已有的程序要花费高昂的时间和人力代价?
5)为什么软件开发和维护的过程仍旧难以度量?
. 种种问题显示了业界对软件以及软件开发方式的关注,这种关注促使了业界对软件工程实践方法的采纳。
1.1.3软件工程
很多人都熟悉计算机编程的思想。他们了解,要使计算机执行某一复杂的任务,而这一任务运行着一项业务,就不得不编写一系列指令来明确规范计算机应该完成的事情。这些指令由诸如C++、Java或C#之类的编程语言编写,形成了我们所熟知的计算机软件。
然而,很少有人知道程序员在开始编写代码之前事先需要完成的工作。程序员不能简单地制定业务操作的规则,或是猜测需要输入系统中的数据类型,这些数据类型会被存储并且稍后会被访问,以屏幕显示或报告的形式提供给用户。
要构建能够适应21世纪挑战的软件产品,就必须认识到以下几个简单的事实:
软件已经深入到我们生活的各个方面,其后果是,对软件应用所提供的特性和功能感兴趣的人们显著增多。当要开发一个新的应用领域或嵌入式系统时,一定会听到很多不同的声音。很多时候,每个人对发布的软件应该具备什么样的软件特性和功能似乎都有着不同的想法。因此,在制定软件解决方案前,必须尽力理解问题。
年复一年,个人、企业和政府的信息技术需求日臻复杂。过去一个人可以构建的计算机程序,现在需要由一个庞大的团队来共同实现。曾经运行在一个可预测、自包含、特定计算环境下的复杂软件,现在可以嵌入到消费类电子产品、医疗设备、武器系统等各种环境上执行。这些基于计算机的系统或产品的复杂性,要求对所有系统元素之间的交互非常谨慎。因此,设计成为软件开发的关键活动。
个人、企业、政府在进行日常运作管理以及战略战术决策时越来越依靠软件。软件失效会给个人和企业带来诸多不便,甚至是灾难性的危害。因此,软件必须保证高质量。随着特定应用感知价值的提升,其用户群和软件寿命也会增加。随着用户群和使用时间的增加,其适应性和可扩展性需求也会同时增加。因此,软件需具备可维护性。
由这些简单事实可以得出一个结论:各种形式、各个应用领域的软件都需要工程化。软件工程是研究如何以系统性的、规范化的、可定量的过程化方法开发和维护软件,以及如何把经过时间考验而证明正确的管理技术和当前能够得到的最好的技术方法结合起来的学科。
软件工程的基础是过程(Process)。软件过程将各个技术层次结合在一起,使得合理、及时地开发计算机软件成为可能。过程定义了一个框架,构建该框架是有效实施软件工程技术必不可少的。软件过程构成了软件项目管理控制的基础,建立了工作环境以便于应用技术方法、提交工作产品(模型、文档、数据、报告、表格等)、建立里程碑、保证质量及正确管理变更。
软件过程是构建工作产品时所执行的一系列活动、动作和任务的集合。活动(Activity)主要实现宽泛的目标(如与利益相关者进行沟通),与应用领域、项目大小、结果复杂性或者实施软件工程的重要程度没有直接关系。动作(Action)(如体系结构设计)包含了主要工作产品(如体系结构设计模型)生产过程中的一系列任务。任务(Task)关注小而明确的目标,能够产生实际产品(如构建一个单元测试)。
在软件工程领域,过程不是对如何构建计算机软件的严格的规定,而是一种可适应性调整的方法,以便于工作人员(软件团队)可以挑选适合的工作动作和任务集合。其目标通常是及时、高质量地交付软件,以满足软件项目资助方和最终用户的需求。
过程框架(Process Framework)定义了若干个框架活动(Framework Activity),为实现完整的软件工程过程建立了基础。这些活动可广泛应用于所有软件开发项目,无论项目的规模和复杂性如何。此外,过程框架还包含一些适用于整个软件过程的普适性活动(Umbrella Activity)。一个通用的软件工程过程框架通常包含以下5个活动。
1)沟通:在技术工作开始之前,和客户(及其他利益相关者)的沟通与协作是极其重要的;其目的是理解利益相关者的项目目标,并收集需求以定义软件特性和功能。
2)策划:如果有地图,任何复杂的旅程都可以变得简单。软件项目好比是一个复杂的旅程,策划活动就是创建一个“地图”,以指导团队的项目旅程,这个地图称为软件项目计划,它定义和描述了软件工程工作,包括需要执行的技术任务、可能的风险、资源需求、工作产品和工作进度计划。
3)建模:无论你是庭园设计家、桥梁建造者、航空工程师、木匠还是建筑师,你每天的工作都离不开模型。你会画一张草图来辅助理解整个项目大的构想—体系结构、不同的构件如何结合,以及其他特征。如果需要,可以把草图不断细化,以便更好地理解问题并找到解决方案。软件工程师也是如此,利用模型来更好地理解软件需求,并完成符合这些需求的软件设计。
4)构建:它包括编码(手写的或者自动生成的)和测试以发现编码中的错误。
5)部署:软件(全部或者部分增量)交付到用户,用户对其进行测评并给出反馈意见。
上述五个通用框架活动既适用于简单小程序的开发,也可用于大型网络应用程序的建造以及基于计算机的大型复杂系统工程。不同的应用案例中,软件过程的细节可能差别很大,但是框架活动都是一致的。
对许多项目来说,随着项目的开展,框架活动可以迭代应用。也就是说,在项目的多次迭代过程中,沟通、策划、建模、构建、部署等活动不断重复。每次项目迭代都会产生一个软件增量(Software Increment),每个软件增量实现部分的软件特性和功能。随着每一次增量的产生,软件逐渐完善。
1.2面向对象的含义
1.2.1什么是面向对象
在现实世界中,一个复杂的事物往往是由许多部分组成的。例如,一辆汽车是由发动机、底盘、车身和车轮等部件组成的。当人们生产汽车时,分别设计和制造发动机、底盘、车身和车轮等,最后把它们组装在一起。组装时,各部分之间有一定联系,以便协同工作。
面向对象系统开发的思路和人们在现实世界中处理问题的思路是相似的,即基于现实世界来设计与开发软件系统的方式。面向对象技术以对象为基础,使用对象抽象现实世界中的事物,以消息来驱动对象执行处理。和面向过程的系统开发不同,面对对象技术不需要一开始就使用一个主函数来概括整个系统的功能,而是从问题域的各个事物入手,逐步构建出整个系统。
在程序结构上,人们常常用下面的公式来表述面向过程的结构化程序:
面向过程程序 = 算法 + 数据结构
算法决定了程序的流程以及函数间的调用关系,也就是函数之间的相互依赖关系。算法和数据结构二者相互独立,分开设计。在实际问题中,有时数据是全局的,很容易超出权限范围修改数据,这意味着对数据的访问是不能控制的,也是不能预测的,如多个函数访问相同的全局数据,因为不能控制访问数据的权限,程序的测试和调试就变得非常困难。另外,面向过程的程序中主函数依赖于子函数,子函数又依赖于更小的子函数。这种自顶向下的模式,使得程序的核心逻辑依赖于外延的细节,一个小小的改动,有可能带来一系列连锁反应,引发依赖关系的一系列变动,这也是过程化程序设计不能很好处理需求变化、代码重用性差的原因。在实践中,人们慢慢意识到算法和数据是密不可分的,通过使用对象将数据和函数封装(或绑定)在一起,程序中的操作通过对象之间的消息传递机制实现,就可以解决上述问题。因此,就形成了面向对象的程序设计:
面向对象程序 = (对象 + 对象 + …) + 消息
面向对象程序设计的任务包括两个方面:一是决定把哪些数据和函数封装在一起,形成对象;二是考虑怎样向对象传递消息,以完成所需任务。各个对象的操作完成了,系统的整体任务也就完成了。
1.2.2对象
对象(Object)是面向对象的基本构造单元,是一些变量和方法的集合,用于模拟现实世界中的一些事物模型,如一台计算机、一个人、一本书等。当然也可以模拟一些虚拟的东西,比如一个学号、一个编号、一个院系等。
事实上,对象是对问题域中某些事物的抽象。显然,任何事物都具有两方面特征:一是该事物的静态特征,如某个人的姓名、年龄、联系方式等,这种静态特征通常称为属性;二是该事物的动态特征,如兴趣爱好、学习、上课、体育锻炼等,这种动态特征称为操作。因此,面向对象技术中任何一个对象都应当具有两个基本要素,即属性和操作。一个对象往往是由一组属性和一组操作构成的。
1.2.3类
为了表示一组事物的本质,人们往往采用抽象方法将众多事物归纳、划分成一些类。例如,我们常用的名词“人”,就是一种抽象表示。因为现实世界只有具体的人,如王安、李晓、张明等。把所有国籍为中国的人归纳为一个整体,称为“中国人”,也是一种抽象。抽象的过程是将有关事物的共性进行归纳、集中的过程。依据抽象的原则进行分类,即忽略事物的非本质特征,只注意那些与当前目标有关的本质特征,从而找出事物的共性,把具有共同性质的事物划分为一类,所得出的抽象概念称为类。
在面向对象的方法中,类的定义如下:
类是具有相同属性和操作的一组对象的集合,它为属于该类的全部对象提供了统一的抽象描述。
事实上,类与对象的关系如同模具和铸件之间的关系。类是对象的抽象定义,或者说是对象的模板;对象是类的实例,或者说是类的具体表现形式。
1.2.4消息
在面向对象系统中,要实现对象之间的通信和任务传递,采用的方法是消息传递。由于在面向对象系统中,各个对象各司其职、相互独立,要使得对象不是孤立存在的,就需要通过消息传递来使其之间发生相互作用。通过对象之间互发消息、响应消息、协同工作,进而实现系统的各种服务功能。
消息通常由消息的发送对象、消息的接收对象、消息传递方式、消息内容(参数)、消息的返回五部分内容组成。消息只告诉接收对象需要完成什么操作,而不告诉接收对象如何完成操作。消息接收者接收它能识别的消息,并独立决定采用什么方法完成所需的操作。
1.2.5封装
封装是指把一个对象的部分属性和功能对外界屏蔽,也就是说从外界是看不到,甚至是不可知的。例如,计算机里面有各种电子元件和电路板,但这些在外面是看不到的,在计算机的外面仅保留用户需要用到的各种按键,即计算机的外部接口。人们使用计算机的时候也不必了解计算机内部的结构和工作原理,只需要知道按键的作用,通过各个外部的按键让计算机执行相应的操作即可。
封装是一种防止相互干扰的方式。所谓封装,包含两层含义:一是将有关的数据和操作封装在一个对象中,形成一个整体,各个对象之间相互对立,互不侵扰。二是将对象中某些属性和操作设置为私有,对外界隐蔽,同时保留少量接口,以便与外界联系,接收外界的消息。这样做既有利于数据安全,防止无关人员修改数据,又可以大大降低人们操作对象的复杂度。使用对象的人完全可以不必知道对象的内部细节,只需了解其外部功能即可自如操作对象。
1.2.6继承
继承是指子类可以自动拥有其父类的全部属性和操作。继承可以指定类从父类中获取特性,同时添加自己的独特特性。例如,已经建立了一个“员工”类,又想另外建立一个“财务人员”类和“销售人员”类,而后两个类与“员工”类的内容基本相同,只是在“员工”类的基础上增加一些属性和操作,显然不必重新设计一个新类,只需在“员工”类的基础上添加部分新内容。继承简化了对现实世界的描述工作,大大提高了软件的复用性。
1.2.7多态
同一条消息被不同的对象接收到时可能产生完全不同的行为,这就是多态性。多态性支持“同一接口,多种方法”的面向对象原则,使高层代码只写一次而在低层可以多次复用。
实际上,在现实生活中可以看到许多多态性的例子。如学校发布一条消息:8月25日新学期开学。不同的对象接收到该条消息后会做出不同的反应:学生要准备好开学上课的必需物品;教师要备好课;教室管理人员要打扫干净教室,准备好教学设施和仪器;宿舍管理人员要整理好宿舍等。显然,这就是多态性。可以设想,如果没有多态性,那么学校就要分别给学生、教师、教室管理人员和宿舍管理人员等许多不同对象分别发开学通知,分别告知需要做的具体工作,这是一件非常复杂的事情。有了多态性,学校在发消息时,不必一一考虑各种类型人员的特点,而不断发送各种消息,只需要发送一条消息,各种类型人员就可以根据学校事先安排的工作机制有条不紊地工作。
从编程角度来看,多态提升了代码的可扩展性。编程人员利用多态性,可以在少量修改甚至不修改原有代码的基础上,轻松加入新的功能,使代码更加健壮,易于维护。
1.3面向对象的有效性
1.3.1面向过程方法的困难
面向过程方法认为客观世界是由一个个相互关联的小系统组成的,各个小系统依据严密的逻辑组成的,环环相扣,井然有序。面向过程方法还认为每个小系统都有着明确的开始和明确的结束,开始和结束之间有着严谨的因果关系。只要将这个小系统中的每一个步骤和影响这个小系统走向的所有因素都分析出来,就能完全定义这个系统的行为。所以如果要分析问题,并用计算机来解决问题,首要的工作是将过程描绘出来,把因果关系都定义出来;再通过结构化的设计方法,将过程进行细化,形成可以控制的、范围较小的部分。通常,面向过程的分析方法是找到过程的起点,然后顺藤摸瓜,分析每一个部分,直至达到过程的终点。这个过程中的每一部分都是过程链上不可分割的一环。事实上,面向过程方法是一种“自顶向下,逐步细分”的解决问题的方法。
在面向过程方法中,计算机解决问题的过程中每一步都会产生、修改或读取一部分数据。每一个环节完成后,数据将顺着过程链传递到下一部分。当需要的最终结果在数据中被反映出来,即达到预期状态的时候,过程就结束了。显然,数据对于问题的解决至关重要。为了更好地管理数据,不至于让系统运行紊乱,人们通过定义主键、外键等手段将数据之间的关系描绘出来,结构化地组织它们。然而随着需求越来越复杂,系统越来越庞大,功能点越来越多,一份数据经常被多个过程共享,这些过程对同一份数据的创建和读取要求越来越趋于复杂和多样,经常出现互相矛盾的数据需求,因此分析和设计也变得越来越困难。同时,这种步步分析的过程分析方法要求过程中的每一步都是预设好的,有着严谨的因果关系。然而,客观世界从来都不是一成不变的,尤其到了信息化时代,外部世界无时无刻不在发生着变化,系统所依赖的因果关系变得越来越脆弱。显然,客观世界的复杂性和频繁变革已经不是面向过程可以轻易应付的了。
1.3.2面向对象方法的有效性
不同于面向过程方法,面向对象程序设计是一种自下而上的程序设计方法,往往从问题的一部分着手,一点一点地构建出整个程序。面向对象设计以数据为中心,类作为表现数据的工具,成为划分程序的基本单位。面向对象是把构成问题的事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
使用计算机解决问题首先要对现实世界进行抽象描述,现实世界中的情况非常复杂,是由独立的事物组成,每一个事物按照最简单的独立方式对应事件,但是在众多大型事物即刻交互的时候,很难进行预测。这种任务使用面向过程方法难以编程。因为,使用面向过程方法基于如下假设:程序结构控制执行流程。因此对于使用过程程序处理上述任务,必须有独立的程序来测试或响应为数众多的变化条件。该问题的解决方案是按照问题自身的相似方式来构建程序,作为独立的软件事物集合,每一个元素都是待抽象的现实世界系统的一个对象。这样就解决了应用域模型和软件域模型之间的冲突,而将劣势转换为优势。
另一方面,图形用户界面(GUI)在20世纪80年代和90年代的快速普及对现代开发方法带来了特殊的困难。GUI引入了之前将仿真编程引入主流商业应用中遇到的问题。原因在于,展示给GUI用户的是计算机显示屏上高度视觉化的界面,一次性提供很多选项操作,每一种选项都可以通过单击鼠标实现。通过下拉菜单、列表框和其他对话框技术,很多其他的选项也可以通过两次或三次单击鼠标来实现。界面开发者自然会探索新技术带来的机遇。结果是,现在系统设计者几乎不可能预测用户通过系统界面执行的任何可能的任务。这意味着计算机应用程序现在很难以过程方式进行设计或控制。面向对象为设计软件提供了自然而然的方法,软件的每一个构件都提供了明确的服务,而这些服务可以被系统的其他部分通过任务序列或控制流独立使用。
在面向对象系统中,信息隐藏表明类有两种定义。外在地,类可以根据接口定义。其他的对象(以及程序员)只需要知道类对象能提供的服务,以及用于请求服务的签名即可。内在地,类可以根据知道的和完成的事情进行定义,但是只有类的对象(以及程序员)需要知道内部定义的详情。遵循以下思路:面向对象的系统可以被构建,以便每一部分的实现基本上独立于其他部分。这就是模块化的思想,并且有助于解决信息系统开发中最棘手的一些问题。模块化的方法可以帮助并以多种方式解决这些问题。按照模块化方式构建的系统易于维护,更新模块化的系统也更加简单。只要替换之前的模块,根据规范采用新的模块,就不会对其他模块产生影响。构建可靠的系统更加容易。子系统可以被单独完整测试多次,而在之后系统集成的时候,需要解决的问题就会少得多。模块化系统可以被开发为小型可管理的增量。假定每一个增量被设计用来提供有用且前后一致的功能包,就可以依次进行部署。
1.4面向对象项目开发
1.4.1面向对象建模
建模在所有工程实践中已得到广泛接受,这主要是因为建模引证了分解、抽象和层次结构的原则。设计中的每个模型都描述了被考虑的系统的某个方面。我们尽可能地在旧模型的基础上构建新模型,因为我们对那些旧模型已经建立起了信心。模型让我们有机会在受控制的条件下失败。我们在预期的情况和特殊的情况下评估每个模型,当它们没能按照我们的期望工作时,我们就修改它们。
为了表达一个复杂系统,必须使用多种模型。例如,当设计一台个人计算机时,电子工程师必须考虑系统的构件视图以及线路板的物理布局。这个构件视图构成了系统设计的逻辑视图,它帮助工程师思考构件间的协作关系。线路板的布局代表了这些构件的物理封装,它受到线路板尺寸、可用电源和构件种类等条件的限制。通过这个视图,工程师可以独立地思考散热和制造等方面的问题。另外,线路板的设计者还必须考虑到在建系统的动态方面和静态方面。因此,电子工程师利用一些图示来展示单个构件之间的静态连接,也用一些时间图来展示这些构件随时间变化的行为。然后,工程师就可以利用示波器和数字分析设备来验证静态模型和动态模型的正确性。
面向对象的建模是一种新的思维方式,一种关于计算和信息结构化的新思维。面向对象的建模,把系统看作是相互协作的对象,这些对象是结构和行为的封装,都属于某个类,那些类具有某种层次化的结构。系统的所有功能通过对象之间相互发送消息来获得。面向对象的建模可以视为一个包含以下元素的概念框架:抽象、封装、模块化、层次、分类、并行、稳定、可重用和可扩展性。
面向对象的建模的出现并不能算是一场技术革命。更恰当地讲,它是面向过程和严格数据驱动的软件开发方法的渐进演变结果。软件开发的新方法受到来自两个方面的推动:编程语言的发展和日趋复杂的问题域的需求驱动。尽管在实际中分析和设计在编程阶段之前进行,但从发展历史看却是编程语言的革新带来设计和分析技术的改变。同样,语言的演变也是对计算机体系的增强和需求的日益复杂的自然响应。
1.4.2面向对象编程
在面向对象编程中,程序被看作是相互协作的对象集合,每个对象都是某个类的实例,所有的类构成一个通过继承关系相联系的层次结构。面向对象的语言常常具有以下特征:对象生成功能、消息传递机制、类和遗传机制。这些概念当然可以并且也已经在其他编程语言中单独出现,但只有在面向对象语言中,它们才共同出现,以一种独特的合作方式互相协作、互相补充。
面向对象系统中,功能是通过与对象的通信获得的。对象可以被定义为一个封装了状态和行为的实体;或者说是数据结构(或属性)和操作。状态实际上是为执行行为而必须存于对象之中的数据、信息。对象的界面,也可称之为协议,是一组对象能够响应的消息的集合。 消息是对象通信的方式,因而也是获得功能的方式。对象收到发给他的消息后,或者执行一个内部操作(有时称为方法或过程),或者再去调用其他对象的操作。所有对象都是类的实例。类是具有相同特点的对象的集合,或者也可以说,类是可用于产生对象的一个模板。对象响应一个消息而调用的方法,由接收该消息的对象自己决定。类可以以一种层次结构来安排。在这个层次结构中,子类可以从比它高的超类中继承得到状态和方法。当对象接收到一个消息后,寻找相应的方法的过程将再从该对象的类开始,并在该类所处的层次结构中展开,最后,直到找到该方法,或者什么也没找到(将会报错)。在某些语言中,一个给定的类可以从不只一个超类中继承,称为多继承。如果采用动态联编,继承就导致了多态性。多态性描述的是如下现象:如果几个子类都重新定义了超类的某个函数(都用相同的函数名),当消息被发送到一个子类对象时,在执行时该消息会根据子类的不同而被解释为不同的操作。 方法也可以被包括在超类的界面中被子类继承,而实际上并不真正定义它。这样的超类也叫抽象类。抽象类不能被实例化,因此也就只能被用于产生子类。
1.4.3面向对象编程语言
面向对象的语言包含4个基本的分支:
1)基于Smalltalk的; 包括Smalltalk的5个版本,以Smalltalk-80为代表。
2)基于C的;包括 Object-C, C++, Java。
3)基于LISP的; 包括 Flavors, XLISP, LOOPS, CLOS。
4)基于Pascal的;包括 Object Pascal, Turbo Pascal, Eiffel, Ada 95 。
Simula实际上是所有这些语言的老祖宗。在这些面向对象语言中,术语的命名和支持面向对象的能力都有不同程度的差别。尽管Smalltalk-80不支持多继承,它仍被认为是最面向对象的语言(the Truest OO Language)。
在基于C的面向对象语言中,Object-C 是Brad Cox开发的,它带有一个丰富的类库,已经被成功用于大型系统的开发。C++是由贝尔实验室的Bjarne Stroustrup编写的。它将C语言中的Struct 扩展为具有数据隐藏功能的Class。多态性通过虚函数(Virtual Function)来实现。C++ 2.0 支持多继承。在多数软件领域,尤其是UNIX平台上,C++都是首选的面向对象编程语言。 同C和C++相类似的新一代基于Internet的面向对象语言Java是由Sun Microsystems研制的。它于1995年伴随着Internet的崛起而风靡一时。用Java写的Applets可以嵌入HTML中被解释执行,这使它具备了跨平台特性。Java和Ada一样支持多线程和并发机制,又像C一样简单、便携。
基于LISP的语言,多被用于知识表达和推理的应用中。其中CLOS(Common LISP Object System)是面向对象LISP的标准版。
在基于Pascal的语言中,Object Pascal是由Niklaus Wirth为Macintosh开发的,它的类库是MacApp。Turbo Pascal 是Borland公司以Object Pascal为范本开发的。
Eiffel由交互软件工程公司的Bertrand Meyer于1987年发布。它的语法类似Ada,运行于UNIX环境。Ada在1983年刚出来时并不支持继承和多态性,因而不是面向对象的。到了1995年,一个面向对象的Ada终于问世,这就是Ada 95。
除了上述的面向对象的语言之外,还有一些语言被认为是基于对象(Object-based)的。它们是:Alphard, CLU, Euclid, Gypsy, Mesa, Modula。
1.4.4面向对象系统开发过程
众所周知,编码并非是软件开发中问题的主要来源。相比之下,需求分析的问题更加普遍,而且它们的纠错代价更加昂贵。因此,对面向对象开发技术的关注就不能仅仅集中在编码上面,更应集中关心系统开发的其他方面。目前最常见的生命周期是“瀑布”模型(结构化)。它是在20世纪60年代末“软件危机”后出现的第一个生命周期模型。如图1-1所示,瀑布式生命周期的开发过程包括以下几个部分:
图1-1瀑布开发模型
(1) 面向对象分析(Object Oriented Analysis, OOA)
软件工程中的系统分析阶段,系统分析员要和用户结合在一起,对用户的需求做出精确的分析和明确的描述,从宏观的角度概括系统应该做什么,而不是怎么做。面向对象分析要按照面向对象的概念和方法,在对任务的分析中,从客观存在的事物和事物之间的关系归纳出有关的对象(包括对象的属性和行为)以及对象之间的联系,并将具有相同属性和行为的对象用一个类来表示,建立一个能真实反映工作情况的需求模型。在这个阶段所形成的模型是比较粗略的。
(2) 面向对象设计(Object Oriented Design, OOD)
根据面向对象分析阶段形成的需求模型,对每一部分分别进行具体的设计,首先是进行类的设计,类的设计可能包含多个层次(利用继承与派生)。然后以这些类为基础提出程序设计的思路和方法,包括对算法的设计。在设计阶段,并不牵涉某一种具体的计算机语言,而是用一种更通用的描述工具(如伪代码或流程图)来描述。
(3) 面向对象编程(Object Oriented Programming, OPP)
根据面向对象设计的结果,用一种计算机语言把它写成程序,显然应当选用面向对象的计算机语言,否则无法实现面向对象设计的要求。
(4) 面向对象测试(Object Oriented Test, OOT)
在写好程序后交给用户使用前,必须对程序进行严格的测试。测试的目的是发现程序中的错误并改正它。面向对象测试是用面向对象的方法进行测试,以类作为测试的基本单元。
(5) 面向对象维护(Object Oriented Maintenance, OOM)
正如对任何商品都需要进行售后服务和维护一样,软件在使用中也会出现一些问题,或者软件商想改进软件的性能,这就需要修改程序。由于使用了面向对象的方法开发程序,使得程序的维护就比较容易了。因为对象的封装性,修改一个对象对其他对象影响很小。利用面向对象的方法维护程序,大大提高了软件维护的效率。
如果所处理的是一个较简单的问题,可以不必严格按照以上五个阶段进行,往往由程序设计者按照面向对象的方法进行程序设计,包括类的设计和程序的设计。
1.4.5面向对象分析与面向对象设计
由于面向对象的技术还比较新,目前存在许多种面向对象的分析和设计方法。面向对象的分析建立于以前的信息建模技术的基础之上,可以定义为是一种以从问题域词汇中发现类和对象的概念来考察需求的分析方法。OOA的结果是一系列从问题域导出的“黑箱”对象。OOA通常使用“剧情”(Scenarios)来帮助确定基本的对象行为。一个剧情是发生在问题域的一个连续的活动序列。在对一个给定的问题域进行OOA时,“框架”(Framework)的概念非常有用。框架是应用或应用子系统的骨架,包含一些具体或者抽象的类。或者说,框架是一个特定的层次结构,包含描述某一问题域的抽象父类。当下流行的所有OOA方法的一个缺点就是它们都缺乏一种固定的模式(Formality)。
在面向对象的设计(OOD)阶段,注意的焦点从问题空间转移到了解空间。OOD是一种包含对所设计系统逻辑的和物理的过程描述,以及系统的静态和动态模型的设计方法。
Booch的OOD技术扩展了他以前在Ada方面的工作。他采用一种“反复综合”(Round-Trip Gestalt)的方法,包括以下过程:识别对象,识别对象的语义,识别对象之间的关系,进行实施,同时包含一系列迭代。Booch是最先使用类图、类分类图、类模板和对象图来描述OOD的人。Wirfs-Brock的OOD技术是由职责代理来驱动的。类职责卡(Class Responsibility Card)被用来记录负责特定功能的类。在确定了类及其职责之后,再进行更详细的关系分析和子系统的实施。Rumbaugh使用三种模型来描述一个系统:① 对象模型,描述系统中对象的静态结构;② 动态模型,描述系统状态随时间变化的情况;③ 功能模型,描述系统中各个数据值的转变。对象图、状态转换图和数据流图分别被用于描述这3个模型。Ivar Jacobson 提出了Objectory方法(或Jacobson法),一种他在瑞典Objective系统中开发的面向对象软件工程方法。Jacobson法特别强调“Use Case”的使用。 Use Case成为分析模型的基础,用交互图(Interaction Diagram)进一步描述后就形成设计的模型。Use Case同时也驱动测试阶段的测试工作。到目前为止,Jacobson法是最为完整的工业方法。
以上所述的方法还有许多变种,无法一一列出。近年来,随着各种方法的演变,它们之间也互相融合。1995年,Booch、Rumbaugh和Jacobson联手合作,提出了第一版的UML(Unified Modeling Language,统一建模语言)。
当组织向面向对象的开发技术转变时,支持软件开发的管理活动也必然要有所改变。承诺使用面向对象技术即意味要改变开发过程、资源和组织结构。面向对象开发的迭代、原型以及无缝性消除了传统开发模式不同阶段之间的界限,新的界限必须被重新确定。同时,一些软件测度的方法也不再适用了。“代码行数”(Lines of Code,LOC)绝对过时了。重用类的数目,继承层次的深度,类与类之间关系的数目,对象之间的耦合度,类的个数以及大小显得更有意义。
资源分配和人员配置都需要重新考虑。开发小组的规模逐步变小,擅长重用的专家开始吃香。重点应该放在重用而非LOC上。重用的真正实现需要一套全新的准则。在执行软件合同的同时,库和应用框架也必须建立起来。长期的投资策略,以及对维护这些可重用财富的承诺和过程,变得更加重要。
至于软件质量保证,传统的测试活动仍是必须的,但它们的计时和定义必须有所改变。例如,将某个功能“走一遍”将牵涉激活一个剧情(Scenario),一系列对象互相作用,发送消息,实现某个特定功能。测试一个面向对象系统是另一个需要进一步研究的课题。发布一个稳定的原型需要不同于以往控制结构化开发的产品的配置管理。
另一个管理方面要注意的问题是合适的工具支持。一个面向对象的开发环境是必须的。同时需要的还包括:一个类库浏览器,一个渐增型编译器,支持类和对象语义的调试器,对设计和分析活动的图形化支持和引用检查,配置管理和版本控制工具,以及一个像类库一样的数据库应用。
除非面向对象开发的历史足以提供有关资源和消耗的数据,否则成本估算也是一个问题。计算公式中应该加入目前和未来的重用成本。最后,管理者也必须明白在向面向对象方法转变的过程中要遇到的风险,如消息传递、消息传递的爆炸增长、动态内存分配和释放的代价。还有一些起步风险,如对合适的工具、开发战略的熟悉,以及适当的培训、类库的开发等。
1.5总结
本章介绍了计算机软件的概念和特点,软件工程的基本概念,以及面向对象的基本概念、重要特性和有效性,并结合系统项目开发,介绍了面向对象软件开发过程等内容。希望通过这一章的学习,读者能够理解并掌握对象、类、消息、封装、继承和多态等概念,了解面向对象系统开发的过程,经典阶段,同时理解面向对象分析和设计的含义及目的,为学习统一建模语言(UML)打下基础。
习题
1. 填空题
(1)面向过程程序可以用公式描述。
(2) 是面向对象程序的基本构造块。
(3)对象都应当具有两个基本要素,即和。
2. 选择题
(1)封装是将对象的结合在一起,形成一个整体。
A.属性和操作 B.消息和事件
C.名称和消息 D.数据集
(2) 可以指定类从父类中获取特性,同时添加自己的独特特性。
A.继承 B.约束
C.映射 D.多态
3. 简答题
(1)比较面向过程方法和面向对象方法,并说明面向对象方法的有效性。
(2)面向对象系统开发过程包含哪些经典阶段?
第2章统一建模语言UML与建模工具Rational Rose
UML是由面向对象领域的三位著名的方法学家Grady Booch、James Rumbaugh和Ivar Jacobson 提出的。这种建模语言得到了工业界的广泛支持,并由OMG(Object Management Group,对象管理组织)采纳作为业界标准。UML 的应用领域很广泛,它可以用于商业建模(Business Modeling),也可以用于其他类型的建模。它是一种通用的建模语言,具有创建系统的静态结构和动态行为等多种结构模型的能力。UML 语言本身并不复杂,它具有可扩展性和通用性,适合为各种多变的系统建模。
2.1模型与建模
2.1.1软件开发模型
模型在软件开发中的使用非常普遍。软件开发通常按以下的方式进行:一旦决定建立一个新的系统,就要写一个非正式的描述说明软件应该做什么,这个描述称为需求说明书(Requirements Specification),通常是经过与系统未来的用户磋商制定的,并且可以作为用户和软件供应商之间正式合同的基础。
完成后的需求说明书移交给负责编写软件的程序员或者项目组,他们去相对隔离地根据说明书编写程序。幸运的话,程序能够按时完成,不超出预算,而且能够满足最初方案目标用户的需要。但不幸的是,在许多情况下事情并非如此。
许多软件项目的失败引发了人们对软件开发方法的研究,试图了解项目为何失败,结果得到了许多对如何改进软件开发过程的建议。这些建议通常以过程模型的形式,描述了开发所涉及的多个活动及其应该执行的次序。
过程模型可以用图解的形式表示。例如,图2-1表示一个非常简单的过程,其中直接从系统需求开始编写代码,没有中间步骤。图中除了圆角矩形表示的过程之外,还显示了过程中每个阶段的产物。如果过程中的两个阶段依次进行,一个阶段的输出通常就作为下一个阶段的输入,如虚线箭头所示。
图2-1软件开发过程模型
开发初期产生的需求说明书可以采取多种形式。书面的说明书可以是所需系统的非常不正规的概要轮廓,也可以是非常详细、井井有条的功能描述。在小规模的开发中,最初的系统描述甚至可能会写不下来,而只是程序员对需求的非正式的理解。在有些情况下,也可能会和未来的用户一起合作开发一个原型系统,成为后续开发工作的基础。上面所述的所有可能性都包括在“需求说明书”这个一般术语中,但并不意味着只有书面的文档才能够作为后继开发工作的起点。还要注意的是,图2-1没有描述整个软件生命周期。一个完整的项目计划还应该提供如项目管理、需求分析、质量保证和维护等关键活动。
单个程序员在编写简单的小程序时几乎不需要比图2-1更多的组织开发过程。有经验的程序员在写程序时心中会很清楚程序的数据和子程序结构,如果程序的行为不是预期的那样,他们能够直接对代码进行必要的修改。在某些情况下,这是完全适宜的工作方式。
然而,对比较大的程序,尤其是如果不只一个人参与开发时,在过程中引入更多的结构通常是必要的。软件开发不再被看作是单独的自由的活动,而是分割为多个子任务,每个子任务一般都涉及一些中间文档资料的产生。
图2-2描述的是一个比图2-1稍微复杂一些的软件开发过程。在这种情况下,程序员不再只是根据需求说明书编写代码,而是先创建一个结构图,用以表示程序的总体功能如何划分为一些模块或子程序,并说明这些子程序之间的调用关系。
图2-2稍复杂的软件开发过程模型
这个过程模型表明,结构图以需求说明书中包含的信息为基础,说明书和结构图在编写最终代码时都要使用。程序员可以使用结构图使程序的总体结构清楚明确,并在编写各个子过程的代码时参考说明书核对所需功能的详细说明。
在开发一个软件期间所产生的中间描述或文档称为模型。图2-2中给出的结构图在此意义上即是一个模型的例子。模型展现系统的一个抽象视图,突出了系统设计的某些重要方面,如子程序和它们的关系,而忽略了大量的低层细节,如各个子程序代码的编写。因此,模型比系统的全部代码更容易理解,通常用来阐明系统的整体结构或体系结构。上面的结构图中包含的子程序调用结构就是这里所指的结构的一个例子。
随着开发的系统规模更大、更复杂以及开发组人数的增加,需要在过程中引入更多的规定。这种复杂性的增加的一个外部表现就是在开发期间使用了更广泛的模型。实际上,软件设计有时就定义为构造一系列模型,这些模型越来越详细地描述系统的重要方面,直到获得对需求的充分理解,能够开始编程为止。
因此,使用模型是软件设计的中心,它具有两个重要的优点,有助于处理重大软件开发中的复杂性。第一,系统要作为整体来理解可能过于复杂,模型则提供了对系统重要方面的简明描述。第二,模型为开发组的不同成员之间以及开发组和外界如客户之间提供了一种颇有价值的通信手段。
2.1.2分析模型与设计模型
在软件开发进入编码之前,通常用模型来帮助理解系统所针对的应用领域。这些模型通常称为分析模型和设计模型,如上面所讨论的结构图。这两类模型可以通过这样的事实区分:分析模型不同于设计模型,它不涉及要开发的系统的任何特性,而是力求捕捉“现实世界”中的业务的某些方面和特性。
总之,分析模型和设计模型满足相同的需要并带来同样的益处。它们支持的或与之相互作用的软件系统和现实世界系统往往都非常复杂,千头万绪。为了管理这种复杂性,系统的描述需要着重于结构而非细节,并要提供系统的一个抽象视图。这个抽象视图确切的特性将依赖于它产生的目的,而且通常需要多个这样的视图或模型为系统提供一个足够的全景。
典型地,分析模型描述应用中处理的数据和处理数据的各种过程。在传统的分析方法中,这些模型用图表示,如逻辑数据模型和数据流图。值得注意的是使用分析模型描述业务过程,早于并且独立于这种过程的计算机化,例如,组织结构图和说明特定生产过程的示意图在商业和工业中已经使用了相当长的时间。
在开发任何有效的软件系统期间,上面定义的分析模型和设计模型很可能都要产生。这就引出了一个问题:它们之间的关系是怎样的?
软件开发过程传统上划分为若干阶段。分析阶段最终以产生一组分析模型而结束,随后是设计阶段,它产生一组设计模型。在这种情况下,分析模型用来形成设计阶段的输入,设计阶段的任务是创建支持分析模型中规定的特性和要求的结构。
这样划分工作有一个问题,在多数情况下,分析和设计模型产物中使用的是完全不同的语言和表示法,这就导致从一个阶段转移到下一个阶段时需要一个翻译过程,分析模型中包含的信息必须用设计模型要求的表示法重新阐述。
显然,这里存在一个危险,就是这个过程容易出错,而且很浪费时间。问题是,如果在开发过程中剩余的阶段要用设计模型取代分析模型,那么为什么还特意创建一个分析模型呢?而且,如果两种模型之间存在表示法上的差异,就难以肯定分析模型中包含的全部信息都准确地提取并且用设计表示法表示。
面向对象技术的一个承诺就是,通过对分析和设计使用同样的模型和建模概念,来消除这些问题。按照这种设想,分析和设计模型之间任何明显的差别都将会消除。显然,设计模型包含分析模型中未表现出来的底层细节,并希望分析模型的基本结构在设计模型中能够保持并且可以直接识别,这样能够消除与分析和设计表示法之间的转换相关的问题。
分析和设计使用相同建模概念的一个后果是这两个阶段之间的区别变模糊了。这个转变最初的动机是希望软件开发能够视为一个“无缝”的过程:分析将标识现实世界系统中的有关对象,并在软件中直接表示这些对象。从这个观点看,设计基本上就是向基础的分析模型中加入详尽的实现细节,分析模型在整个开发过程中将保持不变。
2.2UML简介
2.2.1什么是UML
UML如同它的名字所表明的那样,是一些早期面向对象建模语言的统一。UML的三个主要设计师Grady Booch、James Rumbaugh和Ivar Jacobson在UML出现之前都曾经发表过他们各自的方法,通过将这三种方法深入地理解结合为一体,并发展可用的普遍公认的统一表示法来促进面向对象技术的传播,就产生了UML。
UML 是一种图形化建模语言,它是面向对象分析与设计模型的一种标准表示。UML的目标如下。
1)易于使用,表达能力强,进行可视化建模。
2)与具体的实现无关,可应用于任何语言平台和工具平台。
3)与具体的过程无关,可应用于任何软件开发的过程。
4)简单并且可扩展,具有扩展和专有化机制,便于扩展,无须对核心概念进行修改。
5)为面向对象的设计与开发中涌现出的高级概念(例如协作、框架、模式和构件)提供支持,强调在软件开发中对架构框架模式和构件的重用。
6)与最好的软件工程实践经验集成。
7)可升级,具有广阔的适用性和可用性。
8)有利于面向对象工具的市场成长。
需要说明的是,UML不是一种可视化的程序设计语言,而是一种可视化的建模语言;UML不是工具或知识库的规格说明,而是一种建模语言规格说明,是一种表示的标准;UML不是过程也不是方法,但允许任何一种过程和方法使用它。
2.2.2UML发展历史
从20世纪80年代初期开始众多的方法学家都在尝试用不同的方法进行面向对象的分析与设计。有少数几种方法开始在一些关键性的项目中发挥作用,包括Booch、 OMT、Shlaer/Mellor、 Odell/Martin、 RDD、 OBA 和Objectory 。到了20世纪90年代中期出现了第二代面向对象方法,著名的有Booch?4、 OMT 的沿续以及Fusion 等。此时面向对象方法已经成为软件分析和设计方法的主流。这些方法所做的最重要的尝试是,在程序设计艺术与计算机科学之间寻求合理的平衡,来进行复杂软件的开发。
由于Booch 和OMT 方法都已经独自成功地发展成为世界上主要的面向对象方法,因此Jim Rumbaugh 和Grady Booch 在1994 年10月,共同合作把他们的工作统一起来。到1995 年成为统一方法(Unified Method), 版本0.8。随后,Ivar Jacobson 加入,并采用他的用例(Use Case) 思想,到1996 年,成为统一建模语言版本0.9。 1997 年1月,UML 版本1.0 被提交给OMG 组织,作为软件建模语言标准化的候选。其后的半年多时间里,一些重要的软件开发商和系统集成商都成为UML 伙伴,如Microsoft、 IBM、 HP等。它们积极地使用UML, 并提出反馈意见最后于1997 年9 月再次提交给OMG 组织,于1997 年11 月7 日UML正式被OMG 采纳作为业界标准。UML 的形成过程如图2-3 所示。现在,OMG 已经把UML 作为公共可得到的规格说明(Publicly Available Specification, PAS)提交给国际标准化组织ISO 进行国际标准化。
UML 是Booch、Objectory 和OMT方法的结合,并且是这三者直接的向上兼容的后继。另外它还吸收了其他大量方法学家的思想,包括Wirfs-Brock、Ward、 Cunningham、Rubin、 Harel、 Gamma、 Vlissides、 Helm、 Johnson、 Meyer、 Odell 、Embley、 Coleman、Coad、 Yourdon、 Shlaer 和Mellor。 通过把这些先进的面向对象思想统一起来,UML 为公共的、稳定的、表达能力很强的面向对象开发方法提供了基础。
图2-3UML发展过程
2.2.3UML与软件开发
UML是一种建模语言,是一种标准的表示,而不是一种方法(或方法学)。方法是一种把人的思考和行动结构化的明确方式,方法需要定义软件开发的步骤、告诉人们做什么、如何做、什么时候做以及为什么要这样做。而UML只定义了一些图以及它们的意义,它的思想与方法无关。因此,我们会看到人们将用各种方法来使用UML, 而无论方法如何变化,它们的基础是UML 的图,这就是UML 的最终用途—为不同领域的人们提供统一的交流标准。
软件开发的难点在于一个项目的参与包括领域专家、软件设计开发人员、客户以及用户,他们之间交流的难题成为软件开发的最大难题。UML 的重要性在于,表示方法的标准化有效地促进了不同背景人们的交流,有效地促进软件设计、开发和测试人员的相互理解。无论分析、设计和开发人员采取何种不同的方法或过程,他们提交的设计产品都是用UML 来描述的,有利地促进了相互的理解。
UML 尽可能地结合了世界范围内面向对象项目的成功经验,因而它的价值在于它体现了世界上面向对象方法实践的最好经验,并以建模语言的形式把它们打包,以适应开发大型复杂系统的要求。
在众多成功的软件设计与实现的经验中,最突出的两条:一是注重系统架构的开发,二是注重过程的迭代和递增性。尽管UML 本身没有对过程有任何定义,但UML 对任何使用它的方法(或过程)提出的要求是:支持用例驱动(Use-Case Driven )、以架构为中心(Architecture-Centric )以及递增(Incremental )和迭代(Iterative )的开发。
注重架构意味着不仅要编写大量的类和算法,还要保证这些类和算法之间简单而有效地协作。所有高质量的软件中似乎很多是这类的协作,而近年出现的软件设计模式也正在为这些协作起名和分类,使它们更易于重用。最好的架构就是概念集成(Conceptual Integrity), 它驱动整个项目注重开发模式并力图使它们简单。
迭代和递增的开发过程反映了项目开发的节奏。不成功的项目没有进度节奏,因为它们总是机会主义的,在工作中是被动的,成功的项目有自己的进度节奏,反映在它们有一个定期的版本发布过程,注重对系统架构进行持续的改进。
UML 的应用贯穿在软件开发的五个阶段。
1)需求分析。UML 的用例视图可以表示客户的需求。通过用例建模,可以对外部的角色以及它们所需要的系统功能建模。角色和用例是用它们之间的关系、通信建模的。每个用例都指定了客户的需求:他或她需要系统干什么。不仅要对软件系统,而且对商业过程也要进行需求分析。
2)分析。分析阶段主要考虑所要解决的问题,可用UML 的逻辑视图和动态视图来描述。类图描述系统的静态结构,协作图、序列图、活动图和状态图描述系统的动态特征。在分析阶段,只为问题领域的类建模,不定义软件系统的解决方案的细节(如用户接口的类、数据库等)。
3)设计。在设计阶段,把分析阶段的结果扩展成技术解决方案。加入新的类来提供技术基础结构—用户接口、数据库操作等。分析阶段的领域问题类被嵌入在这个技术基础结构中。设计阶段的结果是构造阶段详细的规格说明。
4)构造。在构造(或程序设计)阶段,把设计阶段的类转换成某种面向对象程序设计语言的代码。在对UML 表示的分析和设计模型进行转换时,最好不要直接把模型转化成代码。因为在早期阶段,模型是理解系统并对系统进行结构化的手段。
5)测试。对系统的测试通常分为单元测试、集成测试、系统测试和接受测试四个不同级别。单元测试是对几个类或一组类的测试,通常由程序员进行;集成测试集成构件和类,确认它们之间是否恰当地协作;系统测试把系统当作一个“黑箱”,验证系统是否具有用户所要求的所有功能;接受测试由客户完成,与系统测试类似,验证系统是否满足所有的需求。不同的测试小组使用不同的UML 图作为他们工作的基础:单元测试使用类图和类的规格说明,集成测试典型地使用构件图和协作图,而系统测试通过用例图来确认系统的行为符合这些图中的定义。
2.2.4UML 的模型、视图、图与系统架构建模
UML是用来描述模型的,它用模型来描述系统的结构或静态特征,以及行为或动态特征。它从不同的视角为系统的架构建模,形成系统的不同视图(View), 包括:
1)用例视图(Use Case View): 强调从用户的角度看到的或需要的系统功能。
2)逻辑视图(Logical View): 展现系统的静态或结构组成及特征,也称为结构模型视图(Structural Model View )或静态视图(Static View)。
3)并发视图(Concurrent View): 体现了系统的动态或行为特征,也称为行为模型视图(Behavioral Model View)或者动态视图(Dynamic View)。
4)构件视图(Component View): 体现了系统实现的结构和行为特征,也称为实现模型视图(Implementation Model View )。
5)部署视图(Deployment View): 体现了系统实现环境的结构和行为特征。
每一种UML 的视图都是由一个或多个图(Diagram) 组成的。一个图就是系统架构在某个侧面的表示,它与其他图是一致的,所有的图一起组成了系统的完整视图。UML 提供了九种不同的图,可以分成两大类,一类是静态图,包括用例图、类图、对象图、构件图、部署图;另一类是动态图,包括序列图、协作图、状态图和活动图。
2.3UML视图、图与建模元素
给复杂的系统建模是一件困难和耗时的事情。从理想化的角度来说整个系统像是一张图,这张图清晰而又直观地描述了系统的结构和功能,既易于理解又易于交流。但事实上,要画出这张图几乎是不可能的。因为,一个简单的图并不能完全反映出系统中需要的所有信息。描述一个系统涉及该系统的许多方面,比如功能性方面(它包括静态结构和动态交互)、非功能性方面(可靠性、扩展性和安全性等)和组织管理方面(工作组、映射代码模块等)。
完整地描述系统,通常的做法是用一组视图反映系统的各个方面,每个视图代表完整系统描述中的一个抽象,显示这个系统中的一个特定的方面。每个视图由一组图构成,图中包含了强调系统中某一方面的信息。视图与视图之间有时会产生轻微的重叠从而使得一个图实际上可能是多个视图的一个组成部分。如果用不同的视图观察系统,每次只集中地观察系统的一个方面。UML 中的视图包括用例视图、 逻辑视图、构件视图、并发视图和部署视图五种,如图2-4所示。
图2-4中的五个视图并不对应于UML中描述的特定的形式构造或图,恰当的说法是每个视图对应一个特定的研究系统的观点。不同的视图突出特定的参与群体所关心的系统的不同方面,通过合并所有五个视图中得到的信息就可以形成系统的完整描述,而为了特殊的目的,只考虑这些视图的子集中包含的信息可能就足够了。从图2-4中可以清晰地看到,用例视图具有将其他四个视图的内容结合到一起的特殊作用。
2.3.1用例视图
用例视图用于描述系统应该具有的功能集,它是从系统的外部用户角度出发对系统的抽象表示。
用例视图所描述的系统功能依靠于外部用户或另一个系统触发激活,为用户或另一个系统提供服务,实现用户或另一个系统与系统的交互。系统实现的最终目标是提供用例视图中描述的功能。
用例视图中可以包含若干个用例(Use-Case)。 用例用来表示系统能够提供的功能(系统用法),一个用例是系统用法(功能请求)的一个通用描述。
用例视图是其他视图的核心和基础。其他视图的构造和发展依赖于用例视图中所描述的内容。因为系统的最终目标是提供用例视图中描述的功能,同时附带一些非功能性的性质。因此,用例视图影响着所有其他的视图。
用例视图还可用于测试系统是否满足用户的需求和验证系统的有效性。
用例视图主要为用户、设计人员、开发人员和测试人员而设置。用例视图静态地描述系统功能。
2.3.2逻辑视图
用例视图只考虑系统应提供什么样的功能,对这些功能的内部运作情况不予考虑,为了揭示系统内部的设计和协作状况,要使用逻辑视图描述系统。
逻辑视图用来显示系统内部的功能是怎样设计的,它利用系统的静态结构和动态行为来刻画系统功能。静态结构描述类、对象和它们之间的关系等。动态行为主要描述对象之间的动态协作,当对象之间彼此发送消息给给定的函数时产生动态协作,以及接口和类的内部结构都要在逻辑视图中定义。
静态结构在类图和对象图中描述,动态建模用状态图、序列图、协作图和活动图描述。
2.3.3构件视图
构件视图用来显示代码构件的组织方式。它描述了实现模块(Implementation Module )和它们之间的依赖关系。
构件视图由构件图构成。构件是代码模块,不同类型的代码模块形成不同的构件,构件按照一定的结构和依赖关系呈现。构件的附加信息(比如,为构件分配资源)或其他管理信息(比如,进展工作的进展报告)也可以加入到构件视图中。构件视图主要供开发者使用。
2.3.4并发视图
并发视图用来显示系统的并发工作状况。并发视图将系统划分为进程和处理机方式,通过划分引入并发机制,利用并发高效地使用资源、并行执行和处理异步事件。除了划分系统为并发执行的控制线程外,并发视图还必须处理通信和这些线程之间的同步问题。并发视图所描述的方面属于系统中的非功能性质方面。
并发视图供系统开发者和集成者(Integrator) 使用。它由动态图(状态图、序列图、协作图、活动图)和执行图(构件图、部署图)构成。
2.3.5部署视图
部署视图用来显示系统的物理架构,即系统的物理部署。比如,计算机和设备以及它们之间的连接方式,其中计算机和设备称为结点,它由部署图表示。部署视图还包括一个映射,该映射显示在物理架构中构件是怎样部署的,比如,在每台独立的计算机上哪一个程序或对象在运行。
部署视图提供给开发者、集成者和测试者使用。
2.3.6UML图
模型通常作为一组图呈现给设计人员。图是一组模型元素的图形化表示。不同类型的图表示不同的信息,一般是它们描述的模型元素的结构或行为。各种图都有一些规则,规定哪些模型元素能够出现在这种图中以及如何表示这些模型元素。UML作为一种可视化的软件分析与设计工具,其主要表现形式就是将模型表示为各种图形,UML视图使用各种UML图诉说关于软件的故事。
UML定义了九种不同类型的图:用例图、类图、对象图、序列图、协作图、状态图、活动图、构件图、部署图。
1.用例图
用例图(Use-Case Diagram) 用于显示若干角色(Actor)以及这些角色与系统提供的用例之间的连接关系,如图2-5所示。用例是系统提供的功能(即系统的具体用法)的描述。通常一个实际的用例采用普通的文字描述,作为用例符号的文档性质。用例图仅仅从角色(触发系统功能的用户等)使用系统的角度描述系统中的信息,也就是站在系统外部察看系统功能,它并不描述系统内部对该功能的具体操作方式。用例图定义的是系统的功能需求。
图2-5用例图示例
2.类图
类图(Class Diagram) 用来表示系统中的类和类与类之间的关系,它是对系统静态结构的描述,如图2-6 所示。
图2-6类图示例
类用来表示系统中需要处理的事物。类与类之间有多种连接方式(关系),比如:关联(彼此间的连接)、依赖(一个类使用另一个类)、泛化(一个类是另一个类的特殊化)等。类与类之间的这些关系都体现在类图的内部结构之中,通过类的属性(Attribute) 和操作(Operation) 反映出来。在系统的生命周期中,类图所描述的静态结构在任何情况下都是有效的。
一个典型的系统中通常有若干个类图。一个类图不一定包含系统中所有的类,一个类还可以加到几个类图中。
3.对象图
对象图是类图的实例,它及时具体地反映了系统执行到某处时系统的工作状况。对象图中使用的图示符号与类图几乎完全相同,只不过对象图中的对象名加了下划线,而且类与类之间关系的所有实例也都画了出来,如图2-7 所示。
图2-7对象图示例
对象图没有类图重要,对象图通常用来示例一个复杂的类图,通过对象图反映真正的实例是什么,它们之间可能具有什么样的关系,帮助对类图的理解。对象图也可以用在协作图中作为其一个组成部分,用来反映一组对象之间的动态协作关系。
4.序列图
序列图用来反映若干个对象之间的动态协作关系,也就是随着时间的流逝,对象之间是如何交互的,如图2-8 所示。序列图主要反映对象之间已发送消息的先后次序,说明对象之间的交互过程,以及系统执行过程中,在某一具体位置将会有什么事件发生。
图2-8序列图示例
序列图由若干个对象组成,每个对象用一个垂直的虚线表示(线上方是对象名),每个对象的正下方有一个矩形条,它与垂直的虚线相叠,矩形条表示该对象随时间流逝的过程(从上至下),对象之间传递的消息用消息箭头表示,它们位于表示对象的垂直线条之间。时间说明和其他的注释作为脚本放在图的边缘。
5.协作图
协作图和序列图的作用一样,反映的也是动态协作。除了显示消息变化(称为交互)外,协作图还显示了对象和它们之间的关系(称为上下文有关)。由于协作图或序列图都反映对象之间的交互,所以建模者可以任意选择一种反映对象间的协作。如果需要强调时间和序列,最好选择序列图;如果需要强调上下文相关,最好选择协作图。
协作图与对象图的画法一样,图中含有若干个对象及它们之间的关系(使用对象图或类图中的符号),对象之间流动的消息用消息箭头表示,箭头中间用标签标识消息被发送的序号、条件、迭代(Iteration) 方式、返回值等,如图2-9所示。通过识别消息标签的语法,开发者可以看出对象间的协作,也可以跟踪执行流程和消息的变化情况。
协作图中也能包含活动对象,多个活动对象可以并发执行。
图2-9协作图示例
6.状态图
一般说来,状态图是对类所描述事物的补充说明,它显示了类的所有对象可能具有的状态,以及引起状态变化的事件,如图2-10 所示。事件可以是另一个对象给它发送的消息或者某个任务执行完毕(比如指定时间到)。状态的变化称作转移(Transition), 一个转移可以有一个与之相连的动作(Action), 这个动作指明了状态转移时应该做些什么。
并不是所有的类都有相应的状态图。状态图仅用于具有下列特点的类:具有若干个确定的状态,类的行为在这些状态下会受到影响且被不同的状态改变。另外,也可以为系统描绘整体状态图。
7.活动图
活动图(Activity Diagram) 反映一个连续的活动流,如图2-11 所示。相对于描述活动流(比如,用例或交互)来说,活动图更常用于描述某个操作执行时的活动状况。
活动图由各种动作状态(Action State) 构成,每个动作状态包含可执行动作的规范说明。当某个动作执行完毕,该动作的状态就会随着改变。这样,动作状态的控制就从一个状态流向另一个与之相连的状态。
图2-10状态图示例
活动图中还可以显示决策、条件、动作状态的并行执行、消息(被动作发送或接收)的规范说明等内容。
图2-11活动图示例
8. 构件图
构件图(Component Diagram) 用来反映代码的物理结构。
代码的物理结构用代码构件表示,构件可以是源代码、二进制文件或可执行文件。构件包含了逻辑类或逻辑类的实现信息,因此逻辑视图与构件视图之间存在着映射关系。构件之间也存在依赖关系,利用这种依赖关系可以很容易地分析一个构件的变化会给其他的构件带来怎样的影响。
构件可以与公开的任何接口(比如OLE / COM )一起显示,也可以把它们组合起来形成一个包(Package), 在构件图中显示这种组合包。实际编程工作中经常使用构件图,如图2-12 所示。
图2-12构件图示例
9. 部署图
部署图(Deployment Diagram) 用来显示系统中软件和硬件的物理架构。通常部署图中显示实际的计算机和设备(用结点表示),以及各个结点之间的关系(还可以显示关系的类型)。每个结点内部显示的可执行的构件和对象清晰地反映出哪个软件运行在哪个结点上。构件之间的依赖关系也可以显示在部署图中。
正如前面所陈述,部署图用来表示部署视图,描述系统的实际物理结构。用例视图是对系统应具有的功能的描述,它们二者看上去差别很大,似乎没有什么联系。然而,如果对系统的模型定义明确,那么从物理架构的结点出发,找到它含有的构件,再通过构件到达它实现的类,再到达类的对象参与的交互,直至最终到达一个用例也是可能的。从整体来说,系统的不同视图对系统的描述应当是一致的,如图2-13所示。
2.3.7UML模型元素
可以在图中使用的概念统称为模型元素。模型元素用语义、元素的正式定义或确定的语句所代表的准确含义来定义。模型元素在图中用其相应的视图元素(符号)表示。利用视图元素可以把图形象、直观地表示出来。一个元素(符号)可以存在于多个不同类型的图中,但是具体以怎样的方式出现在哪种类型的图中要符合(依据)一定的规则。图2-14 给出了类、对象、用例、状态、结点、包(Package) 和构件等模型元素的符号图例。
图2-13部署图示例
图2-14常用模型元素符号图例
模型元素与模型元素之间的连接关系也是模型元素,常见的关系有关联(Association)、泛化(Generalization)、 依赖(Dependency)和实现(Realization),如图2-15 所示。
在UML中,图作为一种可视化的方式聚集了相关需要表达的事物,并且表达了这些事物之间的关系。事物是对模型中最具有代表性的成分的抽象,关系描述了事物之间如何彼此关联、相互依赖或作用的。正是关系把构成系统的诸多事物结合成一个有机的整体。
2.4通用机制和扩展机制
2.4.1通用机制
UML 语言利用通用机制为图附加一些信息,这些信息通常无法用基本的模型元素表示。常用的通用机制有修饰(Adornment)、注释(Note)和规格说明(Specification) 等。
1.修饰
在图的模型元素上添加修饰,为模型元素附加一定的语义。这样,建模者就可以方便地把类型与实例区别开。
当某个元素代表一个类型时,它的名字被显示成黑体字;当用这个元素代表其对应类型的实例时,它的名字下面加下划线,同时还要指明实例的名字和类型的名字。比如,类用长方形表示,其名字用黑体字书写(比如,计算机)。如果类的名字带有下划线,它则代表该类的一个对象(比如,丁一的计算机)。对结点的修饰方式也是一样的,结点的符号既可以是用黑体字表示的类型(比如,打印机),也可以是结点类型的一个实例(丁一的HP 打印机)。其他的修饰有对各种关系的规范说明,比如重数(Multiplicity)。重数是一个数值或一个范围,它指明涉及关系的类型的实例个数,修饰紧靠着模型元素书写。
2.注释
无论建模语言怎样扩展,它不可能应用于描述任何事物。为了在模型中添加一些额外的模型元素无法表示的信息,UML 语言提供了注释。注释可以放在任何图的任意位置,并且可以含有各种各样的信息,信息的类型是字符串。
如果某个元素需要一些解释或说明信息,那么就可以为该元素添加注释,通常用虚线把含有信息的笔记与图中的一些元素联系起来,如图2-16 所示。
3.规格说明
模型元素含有一些性质,这些性质以数值方式体现。一个性质用一个名字和一个值表示,又称作加标签值(Tagged Value)。 加标签值用整数或字符串等类型详细说明。UML 中有许多预定义的性质,比如:文档(Documentation)、 响应(Responsibility)、持续性(Persistence)和并发性(Concurrency)。
性质一般作为模型元素实例的附加规格说明,比如,用一些文字逐条列举类的响应和能力。这种规范说明方式是非正式的,并且也不会直接显示在图中,但是在某些CASE 工具中,通过双击模型元素就可以打开含有该元素所有性质的规格说明窗口,通过该窗口就可以方便地读取信息了。
2.4.2扩展机制
UML 语言具有扩展性,因此也适用于描述某个具体的方法、组织或用户。这里介绍三种扩展机制:构造型(Stereotype )、加标签值(Tagged Value) 和约束(Constrains)。
1.构造型
构造型扩展机制是指在已有的模型元素基础上建立一种新的模型元素。构造型与现有的元素相差不多,只不过比现有的元素多一些特别的语义罢了。构造型与产生该构造型的原始元素的使用场所是一样的。构造型可以建立在所有的元素类型上,比如:类、结点、构件、关系。UML 语言中已经预定义了一些构造型,这些预定义的构造型可以直接使用,从而免去了再定义新构造型的麻烦,使得UML 语言用起来比较简单。
构造型的表示方法是在元素名称旁边添加一个构造型的名字,构造型的名字用字符串(用双尖角括号括起来)表示,如图2-17 所示。
构造型是非常好的扩展机制,它的存在避免了UML 语言过于复杂化,同时也使UML语言能够适应各种需求,很多需求的新模型元素已做成了UML 语言的基础原型(Prototype), 用户可以利用它添加新的语义后定义新的模型元素。
2.加标签值
模型元素有很多性质,性质用名字和值一对信息表示。性质也称为加标签值。UML 语言中已经预定义了一定数量的性质,用户还可以为元素定义一些附加信息,即定义性质。任何一种类型的信息都可以定义为元素的性质,比如,具体的方法信息、建模进展状况的管理信息、其他工具使用的信息、用户需要给元素附加的其他各类信息。
3.约束
约束是对元素的限制。通过约束限定元素的用法或元素的语义。如果在几个图中都要使用某个约束,可以在工具中声明该约束,当然也可以在图中边定义边使用。
图2-18 显示的是老年人类与一般人类之间的关联关系。显然,并不是所有的人都是老年人,为了表示只有60岁以上的人才能加入老年人类,我们定义了一个约束条件:年龄属性大于60岁的人(age > 60 )。有了这个条件,哪个人属于这种关联关系也就自然清楚了。反过来说,假如没有约束条件,这个图就很难解释清楚。在最坏情况下,它可能会导致系统实现上的错误。
图2-18约束图例
2.5UML建模工具概述
使用建模语言需要相应的工具支持,即使人工在白板上画好了模型的草图,建模者也需要使用工具。因为模型中很多图的维护、同步和一致性检查等工作,人工做起来几乎是不可能的。
自从用于产生程序的第一个可视化软件问世以来,建模工具(又叫CASE 工具)一直不很成熟,许多CASE 工具几乎和画图工具一样,仅提供了建模语言和很少的一致性检查,以及一些方法的知识。经过人们不断地改进,今天的CASE 工具正在接近图的原始视觉效果,比如Rational Rose 工具就是一种比较现代的建模工具。但是还有一些工具仍然比较粗糙,比如一般软件中很好用的“剪切”和“粘贴”功能,在这些工具中尚未实现。另外,每种工具都有属于自己的建模语言,或至少有自己的语言定义,这也限制了这些工具的发展。随着统一建模语言UML 的发布,工具制造者现在可能会花较多的时间来提高工具质量,减少定义新的方法和语言所花费的时间。
一个现代的CASE 工具应提供下述功能:
1) 画图(Draw Diagrams)。CASE 工具中必须提供方便作图和为图着色的功能,也必须具有智能,能够理解图的目的,知道简单的语义和规则。这样的特点带来的方便是,当建模者不适当地或错误地使用模型元素时,工具能自动告警或禁止其操作。
2)积累(Repository)。CASE 工具中必须提供普通的积累功能,以便系统能够把收集到的模型信息存储下来。如果在某个图中改变了某个类的名称,那么这种变化必须及时地反射到使用该类的所有其他图中。
3)导航(Navigation)。CASE 工具应该支持易于在模型元素之间导航的功能。也就是,使建模者能够容易地从一个图到另一个图跟踪模型元素或扩充对模型元素的描述。
4)多用户支持。CASE 工具提供该功能使多个用户可以在一个模型上工作,但彼此之间没有干扰。
5)产生代码(Generate Code)。一个高级的CASE 工具一定要有产生代码的能力,该功能可以把模型中的所有信息翻译成代码框架,把该框架作为实现阶段的基础。
6)逆转(Reverse)。一个高级的CASE 工具一定要有阅读现成代码并依代码产生模型的能力,即模型可由代码生成。它与产生代码是互逆的两个过程。对开发者来说,他可以用建模工具或编程两种方法建模。
7)集成(Integrate)。CASE 工具一定要能与其他工具集成,即与开发环境(比如编辑器、编译器和调试器)和企业工具(比如配置管理和版本控制系统)等的集成。
8)覆盖模型的所有抽象层。CASE 工具应该能够容易地从对系统的最上层的抽象描述向下导航至最低的代码层。这样,若需要获得类中一个具体操作的代码,只要在图中单击这个操作的名字即可。
9)模型互换。模型或来自某个模型的个别的图,它应该能够从一个工具输出,然后再输入到另一个工具。就像Java代码可在一个工具中产生,而后用在另一个工具中一样。模型互换功能也应该支持用明确定义的语言描述的模型之间的互换(输出/输入)。