一 UML概述
1 基本概念
UML(统一建模语言,Unified Modeling Language)是OMG(Object Management Group)组织在1997年发表的图标式软件设计语言,是一个绘制软件概念图的图形化记法(notation)。人们可以用它绘制图形,用这些图形来表示一个计划进行的软件设计的问题域,或者用这些图来表示一个已经完成的软件实现。
UML综合了当时很多种已存在的面向对象的建模语言、方法和过程,主要包括:
Booch Method
Object-Oriented Software Engineening
Schlaer-Mellor
Coad-Yourdon
Object Modeing Technique
UML可分为三个种不同的层次:概念层(Conceptual)、规格说明层(Specification)和实现层(Implementation)
概念层上的图形与源代码没有什么严格的关系,它们与人类自然语言相关。它们是用来描述有关已经存在的人类的问题领域的概念和抽象的速记。它们无须遵从严格的语义规则,因此它们的意思理解会有歧义、主题可被解释。
规格说明层和实现层的图形与源代码有明显的关系,实际上,规格说明层的图是准备用来转换成成源代码的,类似地,实现层的图是打算用来描述已经存在的源代码的。在这些层次的图形,有许多规则和语义学要遵从,这些图较少有歧义,基本上都有严格的格式。
举例:一条狗(Dog)是一只动物(Animal)。
表示这句话的一个概念层次的UML图如下
这个图描绘了通过泛化(generalization)关系连接起来的称为Animal(动物)和Dog(狗)的两个实体。Animal是Dog的泛化,一条Dog是一种特定的Animal。这是所有这张图的意义了,没有什么其他意思可以从中推断出来了。这个概念模型没有涉及任何有关计算机、数据处理和程序。我们可以声称,我们的宠物狗是一只动物,我们或者可以谈到属于动物界的生物学的分类上去。因此,这张图是主题可解释的。
不过,这张图在规格说明层次和实现层次上有更明确的意思:
这些代码定义了通过继承关系连接的Animal类和Dog类,这个规格说明模型描述了程序的一部分。
一个概念层次上的图没有定义源代码,也不应该去定义源代码。一个描述了某个问题解决方法的规格说明层次的图,也不会去寻找任何像概念层那样的问题的描述。
2 UML的功能
对一个软件系统来说,UML具有以下主要功能[BOOCH99]:可视化功能;说明功能;建造功能和文档化功能。
可视化(Visualizing)功能
这是非常有价值的,从一个可视化的图上去评估一个系统的依存结构比从代码中去评估容易多了。
可视化可以促进对问题的理解,并且方便设计师彼此交流和沟通。
可以比较容易的发现设计图中可能存在的逻辑错误,避免和减少意外发生。
说明(Specifying)功能
提供了一种通用的,精确的,没有歧义的机制,来对一个软件系统进行说明。
建造(Costructing)功能
UML提供了自己的标准语法规则,可以使用建模工具软件对一个系统设计模型进行解释,并将设计模型映射到计算机语言(如Java)上。也就是说,可以加快系统的设计,实现过程。
通过UML可以反映系统的总貌。这样,当系统设计首先完成后,可以比较容易的发现可以复用的部分,从而降低开发成本。
文档化(Documenting)功能
使用UML进行设计可以同时产生系统设计文档。文档可以帮助开发人员更快的熟悉系统,节省学习时间。
二 UML建模工具
有很多工具可以用来画UML图形。其中有些是纯粹的图形工具,有些则是具有代码生成功能的OO设计工具。包括:
ü Rational ROSE
ü Together
ü Micrisoft Viso
ü Visual UML
ü 纸和白板
三 为什么使用软件模型?
1 为什么使用模型?
问题:搭狗窝和建造纽约世贸中心
为什么工程师要建造模型(models)?为什么航天工程师要建造航天器的模型?为什么桥梁工程师要建造桥的模型?提供这些模型的目的是什么?
提供标准的沟通方式
设计图纸是设计的语言,是工程设计人员,施工人员之间沟通的语言。在一个现代化的工程里,人们要相互沟通和合作,就必须使用标准的工业化设计语言。
通过建立模型来验证事物
工程师建造模型来查明他们的设计是否可以正常工作。航天工程师建造好了航天器的模型,然后把他们放入风洞中了解这些航天器是否可以飞行。桥梁工程师建造桥的模型来了解桥能否接立起来。建筑工程师建造建筑的模型了解客户是否喜欢这种建筑模样。
模型必须是可被检验的。为了检验它,如果一个模型没有一个可用的检验标准,它是相当糟糕的。如果你不能评估一个模型,这个模型是没有价值的。
成本
为什么航天工程师不马上建造一个飞机然后去试飞呢?为什么桥梁工程师不立即建造一座桥然后看它是否可以接立起来呢?因为航天器和桥的造价比模型昂贵多了。当模型比我们实际建造的东西划算多了的时候,我们用模型来研究设计。
2 为什么使用软件建模?
一个UML图可被检验吗?它比创建、检验这个软件更划算吗?
对于这两个问题,这个答案是无法像航天工程师和桥梁工程师那样清楚地了解境况。没有一个检验一个UML图的固定标准。我们能够观察它、评估它,然后应用原则和模式于它,但是最后的评估仍然是相当主观的。画UML图比编写软件花费更少,但不是重要的因素。当然,改变一个UML图比修改源代码容易多了,用UML是不是有意义呢?
当我们需要通过检验确定某些东西的时候,或是使用UML来检验比编码来检验更划算的时候,我们就使用UML。
举一个例子,我有一个特定设计的主意,我需要通过我的团队中的开发人员来考虑它是不是一个好主意去检验,因此,我在白板上画出了一个UML图,然后询问队友们的反馈。
3 是否应该在编码前构造一个全面的设计?
桥梁工程师、航天工程师和建筑工程师都画设计图,为什么呢?因为画一个房子的设计图一个人就可以了,而建造它需要五个或更多人。区区十来个航天工程师能画一个飞机的设计图,而需要上千人去建造它。绘制设计图不需挖掘地基、浇注混凝土和嵌上窗户。简而言之,预先计划一个建筑物远比没有计划的情况下试图建筑它更划算。丢弃一张有错误的设计图花不了多少钱,而拆卸一栋失败的建筑物却要花不少的钱。
软件开发是否应该在编码前构造一个全面的设计?不一定。实际上,许多项目团队在UML图上花费了比编写代码本身更多的时间。弃用一个图比弃用代码是不是更划算,这也不一定。因此,在编写代码前去创建一个全面的UML设计作为一个有价值、有效的选项,也是不一定的。
四 各种UML图形
根据UML图形的用途,可以划分为三类。
静态图(static diagrams)描述了那些不发生变化的软件元素的逻辑结构,描绘了类、对象、数据结构及其存在于它们之间的关系。
动态图(Dynamic diagrams)展示了在运行期间的软件实体的变化,描绘了执行流程、实体改变状态的方式。
物理图(Physical diagrams)显示了软件实体的不变化的物理结构,描绘的物理实体有源文件、库文件、字节文件、数据文件等等,以及存在于它们之间的关系。
类型 |
名称 |
描述 |
静态图 (static diagrams) |
类图 (Class Diagram) |
描述类的结构及类之间的静态关系 |
对象图 (Object Diagram) |
给出系统中对象的快照 |
|
动态图 (Dynamic diagrams) |
用例图 (UseCase Diagram) |
描述一系列的角色和用例间的关系 |
活动图 (Activity Diagram) |
描述不同过程间的动态接触 |
|
序列图 (Sequence Diagram) |
描述不同对象间信息传递的顺序 |
|
状态图 (State Diagram) |
描述对象的内部状态及状态的转移 |
|
协作图 (Collaboration Diagram) |
描述发出信息,接受信息的一系列对象的组织结构 |
|
物理图 (Physical diagrams) |
构件图 (Component Diagram) |
描述可部署的软件构件(如jar文件)之间的静态关系 |
部署图 (Deployment Diagram) |
描述一个系统的拓扑结构 |
通常,最常用的图形是类图(Class Diagram),用例图(UseCase Diagram),状态图(State Diagram)和序列图(Sequence Diagram)。
1 静态图(static diagrams)
描述了那些不发生变化的软件元素的逻辑结构,描绘了类、对象、数据结构及其存在于它们之间的关系。包括:
类图(Class Diagram)
在一个类图中,我们能够查看一个类的属性和方法。我们也能查看一个类是否继承自另外一个类,是否拥有对另外一个类的引用。简而言之,我们能够描绘出类之间的依存关系。
对象图(Object Diagram)
有些时候,将系统在某一特定时刻的状态表示出来是很有用的,特别是当系统结构是动态建立而不是由它类的静态结构表示时。UML对象图就像系统运行时的一个快照(Snapshot),显示了给定实例的对象、关系和属性值。
举例:一个建筑平面设计软件。可以使用GUI画建筑物的平面布置图。程序记录了房间、门、窗户和开了口子的墙,其系统结构下图所示。
这个图能够说明可能会使用哪些类型的数据结构,但是无法告诉你在某个特定的运行时刻有什么对象和关系被实例化了。
设想一下:一个用户使用软件画了两个房间,一个厨房、一个餐厅,它们之间用一堵墙相连着。厨房和餐厅各有一个向外的窗户,餐厅还有一个向外开的门,门是打开着的。这个场景就可以用如下对象图进行描叙。
显示了当时存在于系统中的对象,以及它们被哪些对象关联着。它显示厨房和餐厅是 Space 类的两个独立实例;显示了这两个房间是如何通过墙相连;显示外部空间 实际上是用 Space 类另外一个实例表示;还显示了所有其他必须存在的对象和关系。
需要注意的是不要滥用对象图。当需要对象图时,它们是必不可少的。但是通常,并不是经常需要它们,很多对象图能够由类图直接推断出来,因此它们应用得比较少。为系统的每个场景,甚至每个系统都画对象图是件不可想象的事情!
2 动态图(Dynamic diagrams)
展示了在运行期间的软件实体的变化,描绘了执行流程、实体改变状态的方式。包括:
用例图(UseCase Diagram)
一个用例是有关一个系统的行为的一个描述。描述是从一个用户的观点编写的,这个用户使用系统去做一些特定的事情。一个用例捕获一个事件的可视化序列,这个事件是一个系统对单个用户的激励的响应过程。一个可视化事件是这个用户可以看到的事件,用例不描述任何隐藏着的行为,它们不讨论有关系统的隐藏着的机制,它们仅描述那些用户可以看到的事情。
活动图(Activity Diagram)
略
序列图(Sequence Diagram)
序列图的有三个基本元素:对象、生命线、消息
上面展示了一个典型的序列图,用于描述一个软件系统登录的过程。有关协作的对象被放置在图的顶部,这个人样图标在左边表示一个匿名对象(参与者),它是所有进入和离开这个协作的消息的源头和汇集点。不是所有的序列图都有这样的匿名参与者,但大部分的图都有。
这条垂立在对象或参与者下面的虚线叫做生命线(lifelines)。从一个对象被发送到另外一个对象的消息被画成一个两条生命线之间的箭头。每个消息都被标记出名称。参数被画在消息名称后面的括号里或是一个数据标记(data tokens,一个以小圈结束的箭头)之后。时间(Time)是在垂直方向,因此越是下面的消息是越晚被发送。在 LoginServlet 对象生命线上的小框框叫做活动(Activation),活动是可选的,大多数图都不需要它们。它们表示函数的执行时间。在这个例子中表示 login 函数执行时间长短。 离开这个活动往右的两个消息被 login 方法发送出。那个没有标记的箭头表示 login 函数返回给参与者,传回一个返回值。 注意在 getEmployee消息中的返回变量 e,它表示 getEmployee 的返回值。注意这个 Employee 对象也叫做 e,它们是同一个东西。getEmployee 的返回值是一个Employee 对象的引用。 最后,注意那个 EmployeeDB 是一个类,而不是一个对象,这说明 getEmployee 是一个静态方法。
使用序列图时需要注意:
(1)保持简单
序列图可以展示相当复杂的逻辑,例如下图所示,一个按时付费系统。
不要画这种充斥着大量对象和消息返回的序列图!读懂它们很困难,因此很可能没有人去读,那是极大的时间浪费。相反,应该画一个更小的、能够抓住了你试图做的东西的本质的序列图。每张序列图应该只有一张纸大,留下大量的地方去用文本解释清楚。
(2)不要多画
千万为每一个类的每一个方法建立序列图!这样非常浪费你的时间。
不要画太多的序列图,如果你画得太多,将没有人去看它们。找出相关场景的共性,并将焦点放在这里。对于UML图来说,共性远比差异重要。用你的图去揭示共有的主题和共有的惯例。不要用它们去描述每个小细节。如果你真的需要画一个序列图去描述消息流,节制而简洁地使用它们,尽可能地少用。有时可能根本不需要去画序列图,也许代码能很好的、足够地说明它自己。有代码足够说明它自己时,图是多余的、浪费的。
代码真是能够用于描述系统的一部分吗?实际上,这应当是每个设计者和开发者的目标。团队应该努力去创建表述清楚和可读性强的代码,使更多的代码能够描述自己,更少地需要图。
(3)分割复杂场景
如果你感觉一个真的需要复杂的序列图,看看是否有办法把它们分割成几个小的场景,使其容易理解。
如下图所示,把一个大序列图分解成了更好读的更小的序列图。
(4)使用高层视图
通常,高层视图比低层视图更有用,它们将帮助读者更好地理解系统,它们揭示的共性比差异多。
前面的序列图展现了如何计算一个按时付费的低层操作细节,下图展现了一个系统流程的高层视图。
状态图(State Diagram)
状态图又称做状态转换图(State Transition Diagram)。对象被外界事件激发,从而从一个状态转变为另一个状态。状态图的基本想法是定义一个具有有限个内部状态的机器,因此又称做有限状态机(FSM)。
下图显示了一个简单的状态转换图,描述了如何通过有限状态机控制一个用户登录到一个系统。
图中的矩形(被分为上下两格)表示的是状态。每一个状态的名字显示在在状态图上面的框格中。在状态图的下面的框格中,是当进入和退出这个状态时出发的动作。例如:当我们 开始进入“Prompting for Login”这个状态的时候,我们调用“showLoginScreen”动作;当我们退出“Prompting for Login”这个状态的时候,调用“hideLoginScreen”这个 动作。
箭头表示的是状态之间的转换, 箭头被标以触发转换的事件的名称。当转换被触发的时候,有些箭头还标以要执行的动作。例如:假如我在“Prompting for Login”状态中,并且获得了“login”这个事件,那么我们将转换到“ Validating User”状态,并调用“validateUser”这个动作。
一个实心的黑色圆圈,它叫做初始伪状态(initial pseudo state)。有限状态机的生命周期,是从这个初始状态开始的。 因而,这个例子是从“Prompting for Login”这个状态开始进行转换的。
图中有一个超状态(super state),里面包括了“Sending Password Failed”和“Sending Password Succeeded”两个状态。这两个状态都是通过OK 事件而转换到“Prompting for Login”状态的箭头,所以使用了更简单的一个超状态进行描述