PlantUML活动图
活动图(activity diagram)是UML的5种动态建模机制之一,它阐明了业务用例实现的工作流程。活动图并不像其他建模机制一样直接来源于UML的三位发明人,而是源于Jim Odell的事件图、Petri网和SDL状态建模技术等用于描述工作流和并行过程的建模技术。
1、活动图的基本概念
活动图是状态机的一个特殊例子,它强调计算过程中的顺序和并发步骤。活动图所有或多数状态都是活动状态或动作状态,所有或大部分的转换由源状态(Source State)中活动的完成所触发。
1.1、活动图的定义
活动图是一种用于描述系统行为的模型视图,它可用来描述动作和动作导致对象状态改变的结果,而不用考虑引发状态改变的事件。通常,活动图记录单个操作或方法的逻辑、单个用例或商业过程的逻辑流程。
在UML中,活动的起点用来描述活动图的开始状态,用黑的实心圆表示。活动的终止点描述活动图的终止状态,用一个含有实心圆的空心圆表示。活动图中的活动既可以是手动执行的任务,也可以是自动执行的任务,用圆角矩形表示。状态图中的状态也是用矩形表示,不过活动的矩形与状态的矩形比较起来更加的柔和,更加接近椭圆。活动图中的转换描述一个活动转向另一个活动,用带箭头的实线段表示,箭头指向转向的活动,可以在转换上用文字标识转换发生的条件。活动图中还包括分支与合并、分叉与汇合等模型元素。分支与合并的图标和状态图中判定的图标相同,分叉与汇合则用一条加粗的线段表示。如图所示为一个简单的活动图模型。

活动图可以算是状态图的一种变种,并且活动图的符号与状态图的符号非常相似,有时会让人混淆。我们要注意活动图与状态图的区别。活动图的主要目的是描述动作及对象的改变结果,而状态图则是以状态的概念描述对象、子系统、系统在生命周期中的各种行为。不像正常的状态图,活动图中的状态转换不需要任何触发事件。活动图中的动作可以放在泳道中,而状态图则不可以。泳道可以将模型中的活动按照职责组织起来。
活动图还和传统的流程图很相似,往往流程图所能表达的内容,大多数情况下活动图也可以表达。不过二者之间还是有明显的区别。首先活动图是面向对象的,而流程图是面向过程的。其次,活动图不仅能够表达顺序流程控制,还能够表达并发流程控制。
1.2、活动图的作用
活动图是模型中的完整单元,表示一个程序或工作流,常用于为计算流程和工作流程建模。活动图着重描述了用例实例或对象的活动,以及操作实现中所完成的工作。活动图通常出现在设计的前期,即在所有实现决定前出现,特别是在对象被指定执行的所有活动前。
活动图的作用主要体现在:
- 描述一个操作执行过程中所完成的工作,说明角色、工作流、组织和对象是如何工作的。
- 活动图对用例描述尤其有用,可建模用例的工作流,显示用例内部和用例之间的路径。它可以说明用例的实例是如何执行动作以及如何改变对象状态。
- 显示如何执行一组相关的动作,以及这些动作如何影响它们周围的对象。
- 活动图对理解业务处理过程十分有用。活动图可以画出工作流用以描述业务,有利于与领域专家进行交流。通过活动图可以明确业务处理操作是如何进行的,以及可能产生的变化。
- 描述复杂过程的算法,在这种情况下使用的活动图和传统的程序流程图的功能是相似的。
要注意的是通常活动图假定在整个计算机处理的过程中没有外部事件引起中断,否则普通的状态图更适合描述此种情况。
2、活动图的组成
2.1、动作状态
动作状态(action state)是原子性的动作或操作的执行状态,它不能被外部事件的转换中断。动作状态的原子性决定了动作状态要么不执行,要么就完全执行,不能中断。比如:发送一个信号、设置某个属性值等。动作状态不可以分解成更小的部分,它是构造活动图的最小单位。
从理论上讲,动作状态所占用的处理时间极短,甚至可以忽略不计。而实际上,它需要时间来执行,但是要比可能发生事件需要的时间短得多。动作状态没有子结构、内部转换或内部活动,它不能有由事件触发的转换。动作状态可以有转入,转入可以是对象流或者动作流。动作状态通常有一个输出的完成转换,如果有监护条件也可以有多个输出的完成转换。
动作状态通常用于对工作流执行过程中的步骤进行建模。在一张活动图中,动作状态允许在多处出现。不过动作状态和状态图中的状态不同,它不能有入口动作和出口动作,也不能有内部转移。
在UML中,动作状态使用平滑的圆角矩形表示,动作状态表示的动作写在矩形内部,如图所示。

2.2、活动状态
活动状态是非原子性的,用来表示一个具有子结构的纯粹计算的执行。活动状态可以分解成其他子活动或动作状态,可以使转换离开状态的事件从外部中断。活动状态可以有内部转换、有入口动作和出口动作。活动状态具有至少一个输出完成转换,当状态中的活动完成时该转换激发。
活动状态可以用另一个活动图来描述自己的内部活动。
动作状态是一种特殊的活动状态。可以把动作状态理解为一种原子的活动状态,即它只有一个入口动作,并且它活动时不会被转换所中断。动作状态一般用于描述简短的操作,而活动状态用于描述持续事件或复杂性的计算。一般来说,活动状态的可活动时间是没有限制的。
活动状态和动作状态的表示图标相同,都是平滑的圆角矩形。两者不同的是活动状态可以在图标中给出入口动作和出口动作等信息,如图所示。

2.3、组合活动
组合活动是一种内嵌活动图的状态。把不含内嵌活动或动作的活动称为简单活动,把嵌套了若干活动或动作的活动称为组合活动。
一个组合活动在表面上看是一个状态,但其本质却是一组子活动的概括。一个组合活动可以分解为多个活动或者动作的组合。每个组合活动都有自己的名字和相应的子活动图。一旦进入组合活动,嵌套在其中的子活动图就开始执行,直到到达子活动图的最后一个状态,组合活动结束。与一般的活动状态一样,组合活动不具备原子性,它可以在执行的过程中被中断。
如果一些活动状态比较复杂就会用到组合活动。比如,我们去购物,当选购完商品后就需要付款,虽然付款只是一个活动状态,但是付款却可以包括不同的情况。对于会员来说,一般是打折后付款,而一般的顾客就要全额付款了。这样,在付款这个活动状态中,就又内嵌了两个活动,所以付款活动状态就是一个组合活动。
使用组合活动可以在一幅图中展示所有的工作流程细节,但是如果所展示的工作流程较为复杂,这就会使活动图难以理解。所以,当流程复杂时也可将子图单独放在一个图中,然后让活动状态引用它。
如图所示是一个组合活动的示例。

2.4、分叉与结合
并发(concurrency)指的是在同一时间间隔内,有两个或者两个以上的活动执行。对于一些复杂的大型系统而言,对象在运行时往往不只存在一个控制流,而是存在两个或者多个并发运行的控制流。为了对并发的控制流建模,在UML中引入了分叉和结合的概念。分叉用来表示将一个控制流分成两个或者多个并发运行的分支,结合用来表示并行分支在此得到同步。
分叉和联结在UML中的表示方法相似,都用粗黑线表示。分叉具有一个输入转换,两个或者多个输出转换,每个转换都可以是独立的控制流。如图所示为一个简单的分叉示意图。

结合与分叉相反,结合具有两个或者多个输入转换,只有一个输出转换。先完成的控制流需要在此等待,只有当所有的控制流都到达结合点时,控制才能继续往下进行。如图所示为一简单的结合示意图。

2.5、分支与合并
分支在活动图中很常见,它是转换的一部分,它将转换路径分成多个部分,每一部分都有单独的监护条件和不同的结果。当动作流遇到分支时,会根据监护条件(布尔值)的真假来判定动作的流向。分支的每个路径的监护条件应该是互斥的,这样可以保证只有一条路径的转换被激发。在活动图中,离开一个活动状态的分支通常是完成转换,它们是在状态内活动完成时隐含触发的。要注意的是,分支应该尽可能的包含所有的可能,否则可能会有一些转换无法被激发。这样最终会因为输出转换不再重新激发而使活动图冻结。
合并指的是两个或者多个控制路径在此汇合的情况。合并是一种便利的表示法,省略它不会丢失信息。合并和分支常常成对的使用,合并表示从对应分支开始的条件行为的结束。
要注意区分合并和结合。合并汇合了两个以上的控制路径,在任何执行中,每次只走一条,不同路径之间是互斥的关系。而结合则汇合了两条或两条以上的并行控制路径。在执行过程中,所有路径都要走过,先到的控制流要等其他路径的控制流都到后,才能继续运行。
在活动图中,分支与合并都是用空心的菱形表示。分支有一个输入箭头和两个输出箭头,而合并有两个输入箭头和一个输出箭头。
如图所示为分支与合并的示意图。

2.6、泳道
为了对活动的职责进行组织而在活动图中将活动状态分为不同的组,称为泳道(Swimlane)。每个泳道代表特定含义的状态职责的部分。在活动图中,每个活动只能明确的属于一个泳道,泳道明确地表示了哪些活动是由哪些对象进行的。每个泳道都与其他泳道不同的名称。
每个泳道可能由一个或者多个类实施,类所执行的动作或拥有的状态按照发生的事件顺序自上而下的排列在泳道内。而泳道的排列顺序并不重要,只要布局合理、减少线条交叉即可。
在活动图中,每个泳道通过垂直实线与它的邻居泳道相分离。在泳道的上方是泳道的名称,不同泳道中的活动既可以顺序进行也可以并发进行。虽然每个活动状态都指派了一条泳道,但是转移则可能跨越数条泳道。
如图所示为泳道示例图。

2.7、对象流
活动图中交互的简单元素是活动和对象,控制流(control flow)就是对活动和对象之间的关系的描述。控制流表示动作与其参与者和后继动作之间,以及动作与输入和输出对象之间的关系,而对象流就是一种特殊的控制流。
对象流(object flow)是将对象流状态作为输入或输出的控制流。在活动图中,对象流描述了动作状态或者活动状态与对象之间的关系,表示了动作使用对象以及动作对对象的影响。
关于对象流的几个重要概念有:
- 动作状态
- 活动状态
- 对象流状态
对象是类的实例,用来封装状态和行为。对象流中的对象表示的不仅仅是对象自身,还表示了对象作为过程中的一个状态存在。因此,也可以将这种对象称之为对象流状态(object flow state),用以和普通对象区别。
在活动图中,一个对象可以由多个动作操作。对象可以是一个转换的目的,以及一个活动的完成转换的源。当前转换激发,对象流状态变成活动的。同一个对象可以不止一次的出现,它的每一次出现都表明该对象处于生存期的不同时间点。
一个对象流状态必须与它所表示的参数和结果的类型匹配。如果它是一个操作的输入,则必须与参数的类型匹配。反之,如果它是一个操作的输出,则必须与结果的类型匹配。
活动图中的对象用矩形表示,其中包含带下划线的类名,在类名下方的中括号中则是状态名,表明了对象此时的状态。如图所示为对象示例。

对象流表示了对象与对象、操作或产生它(使用它)的转换间的关系。为了在活动图中把它们与普通转换区分开,用带箭头的虚线而非实线来表示对象流。如果虚线箭头从活动指向对象流状态,则表示输出。输出表示了动作对对象施加了影响,影响包括创建、修改、撤销等。如果虚线箭头从对象流状态指向活动,则表示输入。输入表示动作使用了对象流所指向的对象流状态。如果活动有多个输出值或后继控制流,那么箭头背向分叉符号。反之,如果有多输入箭头,则指向结合符号。
如图所示为包含对象流的活动图。

3、PlantUML语法
3.1、简单活动
- 使用 (
*) 作为活动图的开始点和结束点。 - 有时,你可能想用 (
*top) 强制开始点位于图示的顶端。 - 使用
-->绘制箭头。
js
@startuml
(*) --> "First Activity"
"First Activity" --> (*)
@enduml

3.2、箭头上的标签
- 默认情况下,箭头开始于最接近的活动。
- 可以用
[和]放在箭头定义的后面来添加标签。
js
@startuml
(*) --> "First Activity"
-->[You can put also labels] "Second Activity"
--> (*)
@enduml

3.3、改变箭头方向
可以使用-> 定义水平方向箭头,还可以使用下列语法强制指定箭头的方向:
-down->(default arrow)-right-> or ->-left->-up->
js
@startuml
(*) -up-> "First Activity"
-right-> "Second Activity"
--> "Third Activity"
-left-> (*)
@enduml

3.4、分支
可以使用关键字 if/then/else 创建分支。
js
@startuml
(*) --> "Initialization"
if "Some Test" then
-->[true] "Some Activity"
--> "Another activity"
-right-> (*)
else
->[false] "Something else"
-->[Ending process] (*)
endif
@enduml

不过,有时你可能需要重复定义同一个活动:
js
@startuml
(*) --> "check input"
if "input is verbose" then
--> [Yes] "turn on verbosity"
--> "run command"
else
--> "run command"
endif
-->(*)
@enduml

3.5、更多分支
- 默认情况下,一个分支连接上一个最新的活动,但是也可以使用 if 关键字进行连接。
- 还可以嵌套定义分支。
js
@startuml
(*) --> if "Some Test" then
-->[true] "action 1"
if "" then
-> "action 3" as a3
else
if "Other test" then
-left-> "action 5"
else
--> "action 6"
endif
endif
else
->[false] "action 2"
endif
a3 --> if "last test" then
--> "action 7"
else
-> "action 8"
endif
@enduml

3.6、同步
可以使用 === code === 来显示同步条。
js
@startuml
(*) --> ===B1===
--> "Parallel Activity 1"
--> ===B2===
===B1=== --> "Parallel Activity 2"
--> ===B2===
--> (*)
@enduml

3.7、长的活动描述
- 定义活动时可以用
\n来定义跨越多行的描述。 - 还可以用
as关键字给活动起一个短的别名。这个别名可以在接下来的图示定义中使用。
js
@startuml
(*) -left-> "this <size:20>activity</size>
is <b>very</b> <color:red>long2</color>
and defined on several lines
that contains many <i>text</i>" as A1
-up-> "Another activity\n on several lines"
A1 --> "Short activity <img:sourceforge.jpg>"
@enduml

3.8、注释
- 可以在活动定义之后用 note left, note right, note top or note bottom, 命令给活动添加注释。
- 如果想给开始点添加注释,只需把注释的定义放在活动图最开始的地方即可。
- 也可以用关键字 endnote 定义多行注释。
js
@startuml
(*) --> "Some Activity"
note right: This activity has to be defined
"Some Activity" --> (*)
note left
This note is on
several lines
end note
@enduml

3.9、分区
- 用关键字
partition定义分区,还可以设置背景色 (用颜色名或者颜色值)。 - 定义活动的时候,它自动被放置到最新的分区中。
- 用
}结束分区的定义。
js
@startuml
partition Conductor {
(*) --> "Climbs on Platform"
--> === S1 ===
--> Bows
}
partition Audience #LightSkyBlue {
=== S1 === --> Applauds
}
partition Conductor {
Bows --> === S2 ===
--> WavesArmes
Applauds --> === S2 ===
}
partition Orchestra #CCCCEE {
WavesArmes --> Introduction
--> "Play music"
}
@enduml

3.10、显示参数
用 skinparam 命令修改字体和颜色。
如下场景可用:
- 在图示定义中
- 在引入的文件中
- 在命令行或 ANT 任务提供的配置文件中。
还可以为构造类型指定特殊颜色和字体。
js
@startuml
skinparam backgroundColor #AAFFFF
skinparam activity {
StartColor red
BarColor SaddleBrown
EndColor Silver
BackgroundColor Peru
BackgroundColor<< Begin >> Olive
BorderColor Peru
FontName Impact
}
(*) --> "Climbs on Platform" << Begin >>
--> === S1 ===
--> Bows
--> === S2 ===
--> WavesArmes
--> (*)
@enduml

3.11、八边形活动
可用用 skinparam activityShape octagon 命令将活动的外形改为八边形。
js
@startuml
'Default is skinparam activityShape roundBox
skinparam activityShape octagon
(*) --> "First Activity"
"First Activity" --> (*)
@enduml

3.12、一个完整的例子
js
@startuml
title Servlet Container
(*) --> "ClickServlet.handleRequest()"
--> "new Page"
if "Page.onSecurityCheck" then
->[true] "Page.onInit()"
if "isForward?" then
->[no] "Process controls"
if "continue processing?" then
-->[yes] ===RENDERING===
else
-->[no] ===REDIRECT_CHECK===
endif
else
-->[yes] ===RENDERING===
endif
if "is Post?" then
-->[yes] "Page.onPost()"
--> "Page.onRender()" as render
--> ===REDIRECT_CHECK===
else
-->[no] "Page.onGet()"
--> render
endif
else
-->[false] ===REDIRECT_CHECK===
endif
if "Do redirect?" then
->[yes] "redirect request"
--> ==BEFORE_DESTROY===
else
if "Do Forward?" then
-left->[yes] "Forward request"
--> ==BEFORE_DESTROY===
else
-right->[no] "Render page template"
--> ==BEFORE_DESTROY===
endif
endif
--> "Page.onDestroy()"
-->(*)
@enduml
