UML那些事儿
2.1 类图
2.2 对象图
2.3 包图
2.4 活动图
2.5 序列图
2.6 用例图
类(图A)是对象的蓝图,其中包含3个组成部分。第一个是Java中定义的类名。第二个是属性(attributes)。第三个是该类提供的方法。
属性和操作之前可附加一个可见性修饰符。加号(+)表示具有公共可见性。减号(-)表示私有可见性。#号表示受保护的可见性。省略这些修饰符表示具有package(包)级别的可见性。如果属性或操作具有下划线,表明它是静态的。在操作中,可同时列出它接受的参数,以及返回类型,如图A的“Java”区域所示。
图A
包(图B)是一种常规用途的组合机制。UML中的一个包直接对应于Java中的一个包。在Java中,一个包可能含有其他包、类或者同时含有这两者。进行建模时,你通常拥有逻辑性的包,它主要用于对你的模型进行组织。你还会拥有物理性的包,它直接转换成系统中的Java包。每个包的名称对这个包进行了惟一性的标识。
图B
接口(图C)是一系列操作的集合,它指定了一个类所提供的服务。它直接对应于Java中的一个接口类型。接口既可用图C的那个图标来表示,也可由附加了<<interface>>的一个标准类来表示。通常,根据接口在类图上的样子,就能知道与其他类的关系。
图C
后面的例子将针对某个具体目的来独立地展示各种关系。虽然语法无误,但这些例子可进一步精炼,在它们的有效范围内包括更多的语义。
实体之间一个“使用”关系暗示一个实体的规范发生变化后,可能影响依赖于它的其他实例(图D)。更具体地说,它可转换为对不在实例作用域内的一个类或对象的任何类型的引用。其中包括一个局部变量,对通过方法调用而获得的一个对象的引用(如下例所示),或者对一个类的静态方法的引用(同时不存在那个类的一个实例)。也可利用“依赖”来表示包和包之间的关系。由于包中含有类,所以你可根据那些包中的各个类之间的关系,表示出包和包的关系。
图D
实体之间的一个结构化关系表明对象是相互连接的。箭头是可选的,它用于指定导航能力。如果没有箭头,暗示是一种双向的导航能力。在Java中,关联(图E)转换为一个实例作用域的变量,就像图E的“Java”区域所展示的代码那样。可为一个关联附加其他修饰符。多重性(Multiplicity)修饰符暗示着实例之间的关系。在示范代码中,Employee可以有0个或更多的TimeCard对象。但是,每个TimeCard只从属于单独一个Employee。
图E
聚合(图F)是关联的一种形式,代表两个类之间的整体/局部关系。聚合暗示着整体在概念上处于比局部更高的一个级别,而关联暗示两个类在概念上位于相同的级别。聚合也转换成Java中的一个实例作用域变量。
关联和聚合的区别纯粹是概念上的,而且严格反映在语义上。聚合还暗示着实例图中不存在回路。换言之,只能是一种单向关系。
图F
合成 (图G)是聚合的一种特殊形式,暗示“局部”在“整体”内部的生存期职责。合成也是非共享的。所以,虽然局部不一定要随整体的销毁而被销毁,但整体要么负责保持局部的存活状态,要么负责将其销毁。局部不可与其他整体共享。但是,整体可将所有权转交给另一个对象,后者随即将承担生存期职责。
Employee和TimeCard的关系或许更适合表示成“合成”,而不是表示成“关联”。
图G
泛化(图H)表示一个更泛化的元素和一个更具体的元素之间的关系。泛化是用于对继承进行建模的UML元素。在Java中,用extends关键字来直接表示这种关系。
图H
实例(图I)关系指定两个实体之间的一个合同。换言之,一个实体定义一个合同,而另一个实体保证履行该合同。对Java应用程序进行建模时,实现关系可直接用implements关键字来表示。
图I
本章介绍六类UML图的主要用途,以及常见的概念及图示,以便对这六类图有一个初步的认识。
2.1 类图
如果投票选最重要的UML图,我一定会把票投给类图( class diagram)。类图是一款结构图(structure diagram),如图2-1所示,我们可以用它来表达系统内部重要的组成结构。一个稳定且具弹性的内部结构可以同时支撑系统对外提供的各式服务,以及系统内部复杂的运作,所以我认为类图特别重要。
接下来的各小节会谈到类图中最常见的概念及图示。
2.1.1 类
一群对象(object)享有相同的结构、行为、约束和语义时,称它们是同类(class)的对象。换句话说,定义一个类就相当于描述了一群对象。在类中, 使用属性(attribute)表达对象的结构, 使用操作(operation)表达对象的行为。
如图2-2所示,定义员工(worker)类之后,便可以依据此类的描述产生一群对象。这些:Worker对象不仅可以共用类所定义的属性,拥有自己的属性值,还可以共用类所定义的操作,或者共用约束。
图2-1 类图
图2-2 类与对象
类采用三格的矩形图示,顶格放置类名称,中格放置属性名称,底格放置操作名称。不过,也可以将类的属性格或操作格隐藏起来,节省空间,如图2-3所示。
大多数的UML工具都有隐藏功能。以StarUML为例,点选任何一个类图示都可以选择是否隐藏属性或操作,如图2-4所示。
图2-3 类图示
图2-4 隐藏属性或操作
2.1.2 可见性
对象具有封装(encapsulation)属性,可以把数据结构和行为细节封装起来,外界无法随意存取。对应UML的类概念,我们会看到类中有属性和操作,同时可以设定这些成员是否能被外界存取的可见性(visibility)。以图2-5为例,单笔申购(purchase)封装了一个外界无法存取的私有属性—金额(amount),以及一个外界可以调用的公开操作—计算(calculate)。
图2-5 私有属性与公开操作
目前,UML预设了四种可见性,分别为公开(public)、私有(private)、保护(protected)
和包(package)。公开和私有可见性最常见,也最容易懂,如图2-5所示,减号(-)为私有可见性,加号(+)为公开可见性。
私有可见性滴水不漏,就连子类也无法看见超类的私有成员,这样,其实不利于继承机制。所以,UML设置保护等级的可见性,特别开放子类可以看见超类的保护等级的属性及操作,以便提供更方便的继承机制。保护可见性的符号是井号(#),如图2-6所示。
图2-6 保护等级的属性
最后来谈包可见性。顾名思义,它是为了包而设置的,它的符号是否定号(~),如图2-7所示。同包的类可以看见其他类内部的包属性及操作。所以,从图中可以得知,账户可以看见顾客类的姓名和地址,但是分行(branch)却无法看见,因为分行不是S包的成员。
图2-7 包等级的属性
2.1.3 关联
关联(association)是对象之间最常见的关系,用来连接有结构关系的对象。请看图2-8的例子,关联的图示为实线,实线两端可以连接两个不同的类,如图中的个人(person)类和公司(company)类。
不过,关联的两端也可以连接相同的类,如图2-8中的个人类。虽然,关联两端连接相同的类,但它的链接(link)其实是连接两个不同的实例(instance),只不过这两个实例诞生自相同的类。
图2-8 关联
关联不一定是二元关联(binary association),也可以是多元关联(n-ary association)。多元关联的图示是连接大菱形的实线,如图2-9所示为三元关联(ternary association)。
图2-9 三元关联
有时候会看到带箭头实线,那是在标示导航性(navigation),意味着可以由来源端(source end)导航到箭头所在处的目标端(target end)。如图2-10所示,:Member对象可以链接到:Password对象,但是反向则不成立,也就是说,无法从:Password对象链接到:Member对象,因为两者之间是单向的关联。
图2-10 导航性
特别注意,关联端的标记在UML 2中有所变动,与UML 1版略有不同,如图2-11所示。在
UML 2中,关联的箭头端代表具有导航性。打个小叉就是不可导航;单纯直线代表还未指定可导航或不可导航。所以,回到图2-11的例子中,可以看到
? AB是双向关联。
? CD两端都不具有导航性。
? EF尚未指定两端是否具导航性。
? GH为单向关联,可由G导航到H。
? I端还未决定可否导航,可以确定的是,可由I导航到J。
图2-11 关联图示
2.1.4 多重性
多重性元素(multiplicity element)主要包含一组上下限数,用来指出可被允许生成的实例(instance)数量,即最多可以生成多少数目(上限),最少不得低于多少数目(下限)。关联的两端以“下限..上限”的格式标示出多重性,如图2-12中的1..*。星号(*)代表无指定上限,下限最低为0。如果上下限数相同,标示出一个数目就可以了。因此,可以解读为:
一个顾客(customer)可以拥有一个到多个的账户(account),但是一个账户只能由一个顾客所拥有。
图2-12 多重性
2.1.5 聚合与组合
关联的两端是平等的,没有孰轻孰重的分别。若想表达整体-部分(whole-part)关系,可以改用聚合关系(aggregation)或组合关系(composition)。
聚合与组合都具有整体-部分的特性,唯一的差别在于可否分享(share)。聚合关系中的部件(part object)可以与其他整体(whole object)分享,但是组合关系中的部件则由整体独自拥有。先来看聚合关系,聚合端为空心小菱形,如图2-13所示。
图2-13 聚合关系
因为船(boat)和引擎(engine)之间采用聚合关系,意味着船为整体,而引擎为它的部件。而且,倘若a船被删除了,或者a船不再需要这个引擎而删除之间的链接,b船可以接手使用这个引擎部件,如图2-14所示。
图2-14 部件可重用
但是,如果图2-14改成组合关系,b船就无法重用引擎部件了。因为组合关系中的整体不会分享部件,所以一旦a船被删除,或者a船不再需要这个引擎时,a船都会负责将引擎销毁掉。请看图2-15的例子,组合端为实心小菱形,意味着视窗(window)被删除时,构成视窗的部件都会连带被删除,这是常见的组合关系。
图2-15 组合关系
2.1.6 泛化
实际上,常用继承(inheritance)一词,但是UML没有使用继承这个词汇,不过UML提供了泛化(generalization),来达到子类(subclass)继承超类(superclass)的目的。
泛化将类分为较为泛化的类和较为特化的类,如图2-16所示。通过泛化,子类可以继承超类预先定义好的声明。泛化的图示为带有大三角形箭头的实线,由特化的子类连接指向泛化的超类。
图2-16 泛化
2.1.7 依赖
某一模型元素需要另一个模型元素所提供的规格(specification)或实现(implementation)时,两者之间的关系称为依赖(dependency)。也就是说,少了供应者元素(supplier element)的话,依赖元素(depending element)在语义上(semantically)或者结构上(structurally)可能会不完整(imcomplete)。因此,一旦供应者元素变动,很可能会影响到依赖元素。
例如,结账时需要用到信用卡,所以结账(check out)类依赖信用卡(credit card)类,如图2-17所示。依赖的图示是带箭头虚线,由依赖元素指向供应者元素。
图2-17 依赖
2.1.8 接口
接口(interface)如同契约,负责的类必须负责实现它的公开操作,以及负责维护它的公开属性。以图2-18为例,ProximitySensor类负责实现ISensor接口内部的active操作与read操作,而TheftAlarm类则可以使用ISensor接口。
实际上,可能会先设计出接口与实现者,这种接口特别称为供给接口(provided interface),指由实现类所供给的接口,也可以改用接口独特的圆形图示,如图2-19所示。也可以先设计出使用者以及所需要的接口,这时称这类的接口为需求接口(required interface),其图示为半圆形,如图2-20所示,以便能够一眼区分出该接口为供给接口或需求接口。
图2-18 接口
图2-19 供给接口
图2-20 需求接口
如果把实现者、使用者、需求接口和供给接口全都凑在一起,可以使用图2-21的简图,以
节省图面空间。
图2-21 简图
2.1.9 注释
注释(comment)可以附加在任何元素上,其内放置说明文字,就像3M的便利贴(Post-it)
一样。注释可以用在任何图中,不局限于类图。注释的图示是右上角有折角的矩形,通过虚线连接被注释的元素,如图2-22所示。
图2-22 注释的图示
2.2 对象图
对象图(object diagram)也是一种结构图,如图2-23所示,用来呈现系统在特定时刻的对象(object),以及对象之间的链接(link)。
图2-23 对象图
常说的实例(instance)也会使用对象(object)一词来替换,两者为同义词。系统运行期间,会依据类的定义创建对象,如图2-24所示。
图2-24 类与对象
对象和类共用矩形图示,不过对象名称下方有底线,类名称下方没有底线,如图2-25所示。对象名称经常被省略,所以常见带有冒号的类名称,这其实是个缺名的对象。
图2-25 :Worker是缺名的对象
两个对象之间的关系线称为链接(link),如图2-26所示。
图2-26 链接
2.3 包图
类图、对象图和包图(package diagram),如图2-27所示。包图主要用来为相关的元素分组。对于拥有大量繁杂元素的项目而言,适合用包图来维护管理元素。
图2-27 包图
2.3.1 包
包(package)就像一般的纸箱,可以将相关、欲放置在一起的东西打包成箱。包的图示是上小下大的两个重叠矩形,可以将元素放置其内,如图2-28a所示。也可以将包的内容隐藏起来,形成如图2-28b所示,以节省图面空间。
a) b)
图2-28 包
2.3.2 元素导入
元素导入(element import)可以将包内的任一元素导入到另一个包中。如图2-29所示,元素导入采用带箭头的虚线表示,旁边标上<>关键字,意味着Program包导入了Time数据类型。
图2-29 元素导入
2.3.3 包导入
如果一次导入整个包里的所有元素,可以使用包导入(package import)。如图2-30所示,Order System与Domain DataType之间有包导入的关系,所以Order System内的Employee和Order可以直接使用Domain DataType里的Address和Date。
图2-30 包导入
2.3.4 包合并
顾名思义,包合并(package merge)可将一个包的内容全部合并到另一个包中。换言之,可以经由合并目标包(target package)的内容来扩展来源包(source package)。这好比合并公司,原公司合并了另一家公司,所以原公司就成为一家拥有更多资产的公司了。
如图2-31所示,包合并的图示为带箭头虚线,且于虚线旁标记<>,并由来源包指向目标包,意指将目标包的内容并入来源包。
图2-31 包合并图示
再看图2-32和图2-33所示的例子,会更加清楚。图2-32中的S包合并了Q包之后,形成图2-
33。下面一一解释新的S包的内容:
图2-32 包合并
图2-33 S包
? A—原先的A,加上Q::A。假设原先的A有一个名为name的属性,而Q::A拥有一个名为
address的属性,合并后的新A将同时拥有name和address两个属性。
? B—没有变化。
? C—原先在S包中并不存在C,合并了Q::C。特别是Q::C与Q::A的关联,也会一块并入。
? D—没有变化。
2.4 活动图
活动图是一款行为图(behavior diagram),如图2-34所示,通常用来表达业务流程、工作流或系统流程中一连串的动作。
如图2-35所示,这是一张简单的活动图,用来表达订单的流程。接到订单(receive order)后,决定是否接受这张订单。接受(oreder accepted),就出货(ship order);不接受(order rejected),则结束订单(close order)。
图2-34 活动图
图2-35 活动图
活动图涵盖的概念和图示非常繁杂,在接下来的各小节中,会谈到使用比较多的概念。
2.4.1 动作与控制流
在活动图中,动作(action)是最重要的组成元素,它代表一个执行步骤。动作的图示是圆角矩形,如图2-36所示。
连接动作的带箭头实线称为控制流(control flow)。当来源动作结束之后,控制流会启动目标动作。如图2-37所示,寄送发票(send invoice)的动作执行完之后,会通过控制流启动付款(make payment)动作。
图2-36 动作
图2-37 控制流
2.4.2 对象节点与对象流
对象节点(object node)为矩形图示,对象流(object flow)的图示与控制流相同,不过它的其中一个端点必须是对象节点,而另一端必须是其他节点。控制流的两个端点不可以都是对象节点。
对象流不同于控制流,对象流可以携带数据或对象。若在寄送发票动作结束后,一并传送发票(invoice)到付款处,可以通过对象流,如图2-38所示。
图2-38 对象流
2.4.3 活动参数节点
一般的对象节点出现在活动范围内。如果将对象节点当成活动的参数,用于输入或输出活动,就可以改用活动参数节点(activity parameter node)。
如图2-39所示的范例,放置于活动边框上的三个矩形都是活动参数节点。订单(order)、信用卡(card)和发票(invoice)都是订购处理(order process)活动的参数,它们分别属于Order、CreditCard和Invoice类型。
图2-39 活动参数节点
2.4.4 引脚
引脚(pin)和活动参数节点很像,两者都作为输入/输出使用。差别在于引脚用在动作处,活动参数节点则用在活动处。引脚会提供值(values)给动作,且从动作处接受返回值。
引脚有两种,一种称为输出引脚(output pin),另一种称为输入引脚(input pin)。如图2-40所示,输出引脚用来保存动作的输出值,输入引脚则用来保存动作的输入值。如果想一眼就辨识出输出/输入引脚,也可以采用图2-41的表示法,附箭头的引脚。箭头朝动作外的引脚为输出引脚;箭头朝动作内的引脚为输入引脚。
图2-40 输出引脚与输入引脚
图2-41 带有箭头的引脚
有一种特殊的输入引脚,没有进入线,可以自行提供值,这种输入引脚称为值引脚(value
pin)。以图2-42为例,填写订购交易(fill order)的日期永远都是当日(today)。所以,适合使用值引脚来提供固定的值。值引脚的图示与输入引脚相同,但是需在引脚旁边标记值。
图2-42 值引脚
2.4.5 起点与终点
起始节点(initial node)代表活动流程的起点,整个活动由起始节点开始循着活动边的箭头方向前进。如图2-43所示,起始节点的图示是实心小圆,它没有进入线,但是可以有多条离开线。有始有终,有起始节点,当然就会有终止节点(final node)。UML定义了两种终止节点,如图2-44所示,其一是活动终点(activity final),代表整个活动的终止;另一种是流终点(flow final),代表单一条支流的终止。
图2-43 起始节点
图2-44 活动终点与流终点
活动终点可以有多条进入线,但是无离开线,如图2-45所示。一个活动也可以有多个活动终点,但是任何一条活动边进入任何一个活动终点时,所有支流都会被终止。
图2-45 活动终点
2.4.6 合并
一座山有很多条不同的步道,时而交汇,时而分离。活动流程中,也需要这样的流程交汇点,称为合并节点(merge node)。可以想见,一个合并节点会有多条进入线,但是只有一条离开线,如图2-46所示,合并节点的图示是大的空心菱形,所有进入合并节点的支流都会经历同一条离开线。
图2-46 合并节点图示
2.4.7 判断
判断节点(decision node)与合并节点共用图示,两者都是大的空心菱形。不过,判断节点只有一个进入线,但有多条离开线,如图2-47所示,刚好跟合并节点相反。
图2-47 判断节点图示
虽然判断节点有多条离开线,但只有其中一条离开线可以通过警戒条件(guard)进入下一个活动节点,如图2-48所示。所以,判断节点的离开线上都会附有警戒条件,用来决定一条离开路径。
图2-48 判断节点与警戒条件
2.5 序列图
序列图用来表达系统内部一群对象的交互情况,它是一种行为图,如图2-49所示。
图2-49 序列图
在接下来的各小节中,仅谈论序列图中常见的概念及图示。
2.5.1 交互
交互(interaction)是一个行为单元(behavior unit),用来呈现一群对象互相交换信息的情况。如图2-50所示,使用大方框将一群对象围起来,代表一个交互单元,在大方框内部左上角的框内标示带有关键字sd的交互名称。
图2-50 交互
序列图通常省略交互的大方框,一张序列图的内容就是一个交互单元。既然交互是一个行为单元,当然希望可以重用(reuse)预先设计好的交互,通过组合多个交互单元,形成另一个更大的交互单元。
2.5.2 生命线
生命线(lifeline)代表一个参与交互的实例,它的图示是顶端连接矩形的虚线,如图2-51所示,虚线顶部的矩形可以放置生命线的名称。
图2-51 生命线
2.5.3 执行发生
对象在接收到消息之后执行一项活动,执行期间称为执行发生(execution occurrence),如图2-52所示,它的图示是长条矩形。
图2-52 执行发生
2.5.4 消息
消息(message)的图示是一条带箭头的线段,横跨在两个生命线上,如图2-53所示,对象之间通过发送消息来交互。
图2-53 消息
如图2-54所示,序列图中有四种常见的消息,说明如下:
? 创建消息(createMessage)—顾名思义,用来创建对象的消息称为创建消息。它的图
示是带开放性箭头的虚线,箭头指向目标对象。
? 同步调用(synchCall)—这是最常见的消息。它的图示是带实心箭头的实线,由发送
消息的来源对象指向负责执行的目标对象。
? 回复消息(replyMessage)—目标对象执行结束时,会发出回复消息给来源对象。它的
图示是带开放式箭头的虚线,从负责执行的目标对象反向指回来源对象。
? 异步信号(asynchSignle)—同步与异步的差别在于,来源对象是否等待目标执行结束
才继续往执行。来源对象如果发送同步消息,会等待,如果发送异步消息,就不等待了。
图2-54 四种消息
2.5.5 终止
生命线有生有灭,终止(stop)就是用来表达生命线终止的时刻。终止的图示是一个大叉,放置在生命线的虚线底部,代表生命线已经终止,可连接元素已经不存在,如图2-55所示。
图2-55 终止
2.5.6 一般次序
通常,不同生命线上的事件的发生顺序互不相干。但是,如果想指定顺序,就得使用一般次序(general ordering)。一般次序的图示为中间附箭头的虚线,如图2-56所示,:C接到消息p之后,:O才会发送消息q给:E。
图2-56 一般次序
2.5.7 状态不变式
状态不变式(state invariant)是一种用在生命线上的约束(constraint)。以图2-57为例,购物刷卡时,金额(amount)不能超过信用额度(available credit)。可以在信用卡(credit card)生命线处放置状态不变式。
图2-57 状态不变式
2.6 用例图
用例图(use case diagram)是行为图的一种,如图2-58所示。
图2-58 用例图
用例图用来表达系统对外提供的服务或功能,适合用来作为需求搜集阶段的工件。在接下来的各小节中,会看到用例图中常见的概念及图示。
2.6.1 用例与执行者
针对系统所执行的一连串动作,把它记录起来,即成为用例(use case)。特别注意,用例是行为的规格记录,它除了记载一连串的动作外,还必须记载一连串动作的生成结果,且这个生成可满足系统的执行者(actor)或涉众(stakeholder)。实际上,常用用例来表达系统需求(requirements)或者系统对外呈现的行为(behaviors)。请看图2-59,这是一个自动柜员机的范例,用例采用椭圆图示,名称可放在椭圆内部或底部。执行者是人型图示,由于它会参与系统的运作,因此它跟用例之间有连接线段。
图2-59 用例与执行者
可以将自动柜员机的行为分别记载成三个不同的用例,分别为提款(withdraw)、转账
(transfer funds)和存款(deposit money)。而且,自动柜员机外部一共有两个执行者会参与自动柜员机的行为,一个名为顾客(customer),另一个名为银行(bank)。顾客会参与这三个用例,其中只有存款用例会有两个执行者。
2.6.2 包含关系
基用例(base use case)所描述的行为被定义在另一个被包含用例(included use case)处,两者之间就具有包含关系(include)。以图2-60为例,包含关系是一条有<>的带箭头虚线,由基用例(base use case)指向被包含用例(included use case)。
在图2-60范例中,提款(withdraw)用例中的部分行为定义在卡片验证(card identification)用例里。换言之,卡片验证的行为会被插入到提款的行为中。
图2-60 包含关系
需要特别注意,对于基用例而言,如果缺少被包含用例将无法正确执行,所以只要是采用
包含关系,则意味着被包含用例内的行为一定会被执行。
2.6.3 扩展关系
相对于包含关系一定要执行的特性,扩展关系(extend)则是一种可选择执行的关系。继续以自动柜员机(ATMSystem)为例,打印收据(print receipt)与提款之间的关系就比较适合采用扩展关系,如图2-61所示。在提款流程结束之前,会询问顾客是否需要打印收据,所以打印收据不是一段必要的流程,而是一段可选择的流程。
图2-61 打印收据适合扩展用例
扩展关系的图示与包含关系雷同, 都是带箭头虚线, 差别在于前者的虚线旁设置<>,后者设置<>。另一个很容易混淆的是箭头的方向,扩展关系是由扩展用例(extending use case)指向基用例(base use case),如图2-62所示,包含关系则是由基用例指向被包含用例。
图2-62 扩展关系
2.6.4 扩展点
扩展关系通常会搭配扩展点(extension point)来指明扩展的时机点。以图2-63为例,提款用例记载了名为打印(print)的扩展点,意味着打印收据(print receipt)的行为会插入到打印扩展点处。
图2-63 扩展点