程序员难免要经常画流程图,状态图,时序图等。以前经常用 visio
画,经常为矩形画多大,摆放在哪等问题费脑筋。
有时候修改文字后,为了较好的显示效果不得不再去修改图形。
今天介绍的工具是如何使用PlantUML 的插件画流程图,状态图,时序图等。这是一种程序员看了就会爱上的画图方式:自然,高效。
什么是 PlantUML
PlantUML 是一个画图脚本语言,用它可以快速地画出:
类图:http://plantuml.com/class-diagram
流程图:http://plantuml.com/activity-diagram-beta
时序图:http://plantuml.com/sequence-diagram
用例图:http://plantuml.com/use-case-diagram
状态图:http://plantuml.com/state-diagram
组件图:http://plantuml.com/component-diagram
它的原理值得知道:你写的是文本,图是布局引擎排出来的。PlantUML 解析脚本后,把“谁和谁有什么关系”交给自动布局算法(经典做法是调用 Graphviz 的 dot)算出每个框的位置和连线走向,再渲染成图片。这就是它和 Visio 的根本区别——程序员不摆矩形,只描述结构。代价是布局不能完全手工控制,收益是改文字不用改图、脚本可以进 git 做版本管理、评审时看 diff 就知道图改了哪里。下面按图型给案例,并把“脚本怎么读”讲清楚。
1. 类图
类图描述类型之间的静态关系,读图先读箭头:
<|-- 实线空心三角:继承(extends),三角指向父类;
<|- 虚线空心三角:实现(implements),三角指向接口;
--> 实线箭头:关联(一个类持有另一个类的引用);
o-- 空心菱形:聚合(整体与部分,部分可独立存在);*-- 实心菱形:组合(部分随整体存亡)。本文案例只用到前三种。
类体花括号里直接写成员,一行字段、一行方法;abstract class、interface、enum 关键字决定图形样式(斜体类名、«interface» 标记等)。
案例1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@startuml
abstract class AbstractList
abstract AbstractCollection
interface List
interface Collection
List <|-- AbstractList
Collection <|-- AbstractCollection
Collection <|- List
AbstractCollection <|- AbstractList
AbstractList <|-- ArrayList
class ArrayList {
Object[] elementData
size()
}
enum TimeUnit {
DAYS
HOURS
MINUTES
}
@enduml
|

案例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@startuml
class Track
class Media
class Trip{
String tripID;
String tracks;
String medias;
}
Trip --> Track
Trip --> Media
interface ITripTrackCollection{
void start();
void stop();
void pause();
void destory();
}
class TripTrackCollection implements ITripTrackCollection{
Vector<LocationInfo> mLocations;
ExtcutorService mVecoterThread;
ScheduledExecutorService mDatabaseThread;
}
class TrackCollectService extends Service implements ITripTrackCollection{
TripTrackCollection TripTrackCollection;
}
TrackCollectService -->TripTrackCollection
@enduml
|

2. 流程图
PlantUML 的活动图有两代语法:案例1 是旧语法,(*) 表示起止点、If … then … else … Endif 表示分支;案例2 是新语法,start/stop 表起止、:动作; 是一个处理框、if (条件) then (yes) … else (no) … endif 是分支,还支持嵌套。两代语法表达力相同,同一张图里不要混用,新项目建议直接用新语法。
案例1:
1
2
3
4
5
6
7
8
9
10
|
@startuml
(*) --> "check input"
If "input is verbose" then
--> [Yes] "turn on verbosity"
--> "run command"
else
--> "run command"
Endif
-->(*)
@enduml
|

案例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
start
:"步骤1处理";
:"步骤2处理";
if ("条件1判断") then (true)
:条件1成立时执行的动作;
if ("分支条件2判断") then (no)
:"条件2不成立时执行的动作";
else
if ("条件3判断") then (yes)
:"条件3成立时的动作";
else (no)
:"条件3不成立时的动作";
endif
endif
:"顺序步骤3处理";
endif
if ("条件4判断") then (yes)
:"条件4成立的动作";
else
if ("条件5判断") then (yes)
:"条件5成立时的动作";
else (no)
:"条件5不成立时的动作";
endif
endif
stop
@enduml
|

3.时序图
时序图回答“谁在什么时候调了谁”:
- 每个参与者是一条垂直生命线,
-> 实线是调用消息,--> 虚线是返回;
- 参与者按首次出现的顺序从左到右排,想控制顺序就先用
participant 声明一遍(案例2 还用 #颜色 给不同模块的参与者上色、autonumber 给消息自动编号);
A -> A : msg 是自调用,画出来是生命线上折返的小箭头(案例2 里 ActivityManagerService 的多处调用就是)。
案例1:
1
2
3
4
5
6
7
|
@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
Alice -> Bob: Another authentication Request
Alice <-- Bob: another authentication Response
@enduml
|

案例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
@startuml
title Android Broadcast procedure
participant Activity #Lime
participant ContextWrapper #Cyan
participant ContextImpl #Cyan
participant ActivityManagerService #Cyan
participant ActivityStackSupervisor #Cyan
participant ActivityStack #Cyan
participant ApplicationThreadProxy #Silver
participant InnerReceiver #Magenta
participant ReceiverDispatcher #Magenta
participant BroadcastReceiver #Magenta
autonumber
Activity -> ContextWrapper : registerReceiver()
ContextWrapper -> ContextImpl : registerReceiver()
ContextImpl -> LoadedApk : getReceiverDispatcher()
LoadedApk -> ActivityManagerProxy : registerReceiver()
ActivityManagerProxy -> ActivityManagerService : registerReceiver()
Activity -> ContextWrapper : sendBroadcast()
ContextWrapper -> ContextImpl : sendBroadcast()
ContextImpl -> ActivityManagerService: broadcastIntent()
ActivityManagerService -> ActivityManagerService : broadcastIntentLocked()
ActivityManagerService -> ActivityManagerService : collectReceiverComponents()
ActivityManagerService -> ActivityManagerService : scheduleBroadcastsLocked()
ActivityManagerService -> ActivityManagerService : processNextBroadcast()
ActivityManagerService -> ActivityManagerService : deliverToRegisteredReceiverLocked()
ActivityManagerService -> ActivityManagerService : performReceiveLocked()
ActivityManagerService -> ApplicationThreadProxy : scheduleRegisteredReceiver()
ApplicationThreadProxy -> InnerReceiver : performReceive()
InnerReceiver -> ReceiverDispatcher : performReceive()
ReceiverDispatcher -> BroadcastReceiver : onReceive()
Activity -> ContextWrapper : sendOrderedBroadcast()
ContextWrapper -> ContextImpl : sendOrderedBroadcast()
ContextImpl -> ActivityManagerService: broadcastIntent()
@enduml
|

4. 用例图:
用例图站在用户视角描述系统能力::名字: 定义参与者(小人),(名字) 定义用例(椭圆),箭头表示谁发起哪个用例;note … end note 挂说明文字,as 起别名方便后面引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@startuml
:Main Admin: as Admin
(Use the application) as (Use)
User -> (Start)
User --> (Use)
Admin ---> (Use)
note right of Admin : This is an example.
note right of (Use)
A note can also
be on several lines
end note
note "This note is connected\nto several objects." as N2
(Start) .. N2
N2 .. (Use)
@enduml
|

5. 状态图
状态图描述一个对象生命周期里的状态迁移:[*] 是初始伪状态(也用作终态箭头的终点),A --> B : 事件 表示“事件发生时从 A 迁到 B”;状态可以嵌套成组合状态(案例里 NotShooting、Configuring 都是带内部子状态的大状态),把同一层次的行为收拢在一起——这是它比流程图更适合描述对象行为的原因。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@startuml
scale 350 width
[*] --> NotShooting
state NotShooting {
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 -> State2
}
}
@enduml
|

6. 组件图
组件图描述部署/模块视角的结构:[名字] 是组件,容器有语义分级——package 打包、node 物理节点(一台机器)、cloud 云、database 数据库,里面还能再嵌 folder/frame。读图先看容器分层(哪些组件跑在哪个节点/云里),再看 --> 的依赖方向。
案例1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@startuml
package "Some Group" {
HTTP - [First Component]
[Another Component]
}
package "Other Groups" {
FTP - [Second Component]
[First Component] --> FTP
}
@enduml
|

案例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@startuml
package "组件1" {
["组件1.1"] - ["组件1.2"]
["组件1.2"] -> ["组件2.1"]
}
node "组件2" {
["组件2.1"] - ["组件2.2"]
["组件2.2"] --> [负载均衡服务器]
}
cloud {
[负载均衡服务器] -> [逻辑服务器1]
[负载均衡服务器] -> [逻辑服务器2]
[负载均衡服务器] -> [逻辑服务器3]
}
database "MySql" {
folder "This is my folder" {
[Folder 3]
}
frame "Foo" {
[Frame 4]
}
}
[逻辑服务器1] --> [Folder 3]
[逻辑服务器2] --> [Frame 4]
[逻辑服务器3] --> [Frame 4]
@enduml
|

踩坑记录与注意事项
- 本地渲染依赖 Java 和 Graphviz:报 “dot” 相关错误基本是没装 Graphviz;新版 PlantUML 内置了纯 Java 布局引擎,加
-Playout=smetana 可以不装 Graphviz 直接渲染;
- 中文乱码:命令行渲染加
-charset UTF-8,并确认 .plantuml 文件本身存的是 UTF-8;个别环境还要用 skinparam defaultFontName 指定含中文字形的字体;
- 箭头方向别写反:
<|-- 的三角永远指向父类/接口那一端,写反了继承关系就整个颠倒;
- 时序图参与者顺序 = 首次出现顺序:想让谁排在左边就先用 participant 声明谁,后面消息写乱只影响线不影响列序;
- 流程图新旧语法别混用:
(*) 旧语法与 start/stop 新语法混在一张图里会解析出错;
- 布局不可手调是设计取舍:嫌线拐得丑,优先调整声明顺序、加方向参数(如 left to right direction),而不是找“拖动”功能——它没有。