Mermaid 类图编辑器
类图展示类型以及它们之间的关系:谁包含谁、谁继承谁、谁依赖谁。当代码的结构本身就是重点时用它——领域模型、插件接口、继承树。如果你想展示的是运行时发生了什么,而不是类型如何拼装,那该用时序图。
一个支付领域模型
一张图里包含三种关系:用组合表示不能脱离整体存在的部分,用继承表示支付方式的层次,以及一条带多重性的普通关联。这里的含义主要由关系箭头承载——类的方框反而次要。
classDiagram
class 订单 {
+String 订单号
+订单状态 状态
+金额 总计()
+void 添加明细(商品 p, int 数量)
}
class 订单明细 {
+商品 商品
+int 数量
+金额 小计()
}
class 支付方式 {
<<abstract>>
+授权(金额 amount) bool
}
class 信用卡 {
+String 卡号后四位
+授权(金额 amount) bool
}
class 银行转账 {
+String 账号
+授权(金额 amount) bool
}
订单 "1" *-- "1..*" 订单明细 : 包含
订单 --> 支付方式 : 支付使用
支付方式 <|-- 信用卡
支付方式 <|-- 银行转账实例讲解
1. 一个类
`+` 表示公有,`-` 表示私有,`#` 表示受保护。带括号的成员会被画成方法,不带括号的是字段。
classDiagram
class 用户 {
+String 邮箱
-String 密码哈希
+bool 校验(String 输入)
}2. 继承与接口
`<|--` 表示继承,读作“右边的那个扩展左边的那个”。`<<interface>>` 只是一个标注而非行为,但正是它让图变得可读。
classDiagram
class 仓储 {
<<interface>>
+查找(String id) 实体
+保存(实体 e) void
}
class Postgres仓储 {
-连接 conn
+查找(String id) 实体
+保存(实体 e) void
}
class 内存仓储 {
-Map 存储
+查找(String id) 实体
+保存(实体 e) void
}
仓储 <|.. Postgres仓储
仓储 <|.. 内存仓储3. 组合与聚合的区别
区别在于生命周期。实心菱形(`*--`)表示部分随整体一起消亡——删掉发票,它的明细也就没了。空心菱形(`o--`)表示部分可以独立存在。
classDiagram
class 发票 {
+String 发票号
}
class 发票明细 {
+String 品名
}
class 客户 {
+String 名称
}
发票 "1" *-- "1..*" 发票明细 : 由其构成
客户 "1" o-- "0..*" 发票 : 已开具4. 泛型
波浪号用来写类型参数:`仓储~用户~`。也可以嵌套,偶尔确实需要,但很少是好主意。
classDiagram
class 仓储~T~ {
+查找(String id) T
+全部() List~T~
}
class 缓存~K, V~ {
+取(K key) V
+存(K key, V value) void
}
class 用户仓储 {
+按邮箱查找(String 邮箱) 用户
}
仓储~用户~ <|-- 用户仓储5. 备注与方向
`direction LR` 让图从左到右排布,对继承树来说通常比默认方向更合适。备注则是安放那些塞不进类框的约束的好地方。
classDiagram
direction LR
class 事件存储 {
+追加(事件 e) void
+重放(String 流ID) List~事件~
}
class 快照 {
+int 版本
+byte[] 内容
}
事件存储 --> 快照 : 每 100 个事件写一次
note for 事件存储 "只追加。事件永不修改、永不删除。"类图语法速查
关系箭头是最值得记住的部分——它们才是类图区别于“方框加连线”的地方,而且它们的读法是从箭头往回读,这一点常让人搞反。
| 语法 | 含义 |
|---|---|
| classDiagram | 开启该图。区分大小写。 |
| class 名称 { ... } | 带成员的类。右花括号要单独占一行。 |
| +成员 | 公有。 |
| -成员 | 私有。 |
| #成员 | 受保护。 |
| +方法(类型 参数) 返回类型 | 方法——括号才是它成为方法的原因。 |
| <<interface>> / <<abstract>> | 构造型标注,写在类体的第一行。 |
| A <|-- B | 继承:B 扩展 A。 |
| A <|.. B | 实现:B 实现接口 A。 |
| A *-- B | 组合:B 不能脱离 A 存在。 |
| A o-- B | 聚合:B 可以脱离 A 存在。 |
| A --> B | 有方向的关联。 |
| A ..> B | 依赖——A 使用 B,但并不持有它。 |
| A "1" --> "0..*" B : 文字 | 两端的多重性加上关系标签。 |
| class 仓储~T~ | 泛型类型参数。 |
| note for A "文字" | 附加在某个类上的备注。 |
| direction LR | 改变布局方向。 |
会让类图出问题的几种情况
均以 Mermaid 11.12.2 复现。类图比这里大多数图类型更宽容,所以其中好几种会照常渲染,只是给你一张错的图。
你会看到
Parse error,结尾是:got 'EOF_IN_STRUCT'
原因
用 `{` 打开的类体没有闭合。这个标记名难得地直白:文件结束时解析器还停在某个类里面。
解决办法
把右花括号单独放一行闭合。
classDiagram
class 订单 {
+String 订单号classDiagram
class 订单 {
+String 订单号
}你会看到
Parse error,结尾是:got 'ANNOTATION_END'
原因
在类图里用了时序图的箭头。`->>` 在这里没有含义,而解析器已经吃进去一部分,于是吐出一个令人费解的标记名。
解决办法
改用类图的关系:关联用 `-->`,继承用 `<|--`,组合用 `*--`。
classDiagram
订单 ->> 客户classDiagram
订单 --> 客户 : 属于你会看到
No diagram type detected matching given configuration
原因
关键字大小写写错。`classdiagram` 不是 `classDiagram`。
解决办法
把 D 大写。
classdiagram
class 订单classDiagram
class 订单你会看到
箭头指向和你想表达的正好相反
原因
类图的关系箭头要从箭头端往回读。`A <|-- B` 表示 B 继承自 A,而不是反过来。写反了照样能渲染——只不过它现在宣称你的基类继承自它自己的子类。
解决办法
读作“远端扩展尖端”。父类写在 `<|--` 的左边。
classDiagram
信用卡 <|-- 支付方式classDiagram
支付方式 <|-- 信用卡你会看到
本该是方法的地方渲染成了字段
原因
区分方法和字段的唯一依据就是括号。`+保存` 是一个叫“保存”的字段,`+保存()` 才是方法。两者都合法,所以不会有任何提示。
解决办法
补上括号;如果想显示返回类型,就写在括号后面。
classDiagram
class 仓储 {
+保存
+查找
}classDiagram
class 仓储 {
+保存(实体 e) void
+查找(String id) 实体
}你会看到
组合与聚合乍看一样,含义却相反
原因
`*--` 和 `o--` 只差一个字符,却编码了一个真实的语义差别:部分能否脱离整体存活。用错了会得到一张形式上完全正确、事实上却在歪曲你的领域的图。
解决办法
删除父对象会连带删掉子对象时用实心菱形 `*--`;不会时用空心的 `o--`。
classDiagram
订单 o-- 订单明细 : 包含classDiagram
订单 *-- 订单明细 : 包含渲染须知
以下均为针对本站所用 Mermaid 11.12.2 的实测结果。
中文成员名让类框更窄
类框的宽度由最长的那一条成员签名决定。汉字单字更宽,但表达同样含义所需的字数少得多,净效果是中文类图通常比英文原版更窄。这一点在类图上尤其有价值,因为宽度本来就是由单条最长签名主导的——`+按邮箱查找(String 邮箱) 用户` 通常比 `+findByEmail(String email) User` 更短。
类图在高度上的增长快过这里任何其他类型
3 个类的 viewBox 约为 94×610,40 个类是 108×6900——大约每个类 172 像素高,是本站六种图里增长最陡的。四十个类的图接近七千像素高,作为单张图片已经没法用了。`direction LR` 有帮助,但超过大约十五个类之后,诚实的办法是按限界上下文把图拆开。
成员数量几乎不影响宽度
决定宽度的是最长的那一条成员签名,而不是成员有多少条。一个有二十个短字段的类,并不比只有三个字段的类更宽。也就是说,成员可以大方一些、类要吝啬一些——这和大多数人的直觉正好相反。
标签是 HTML,所以 PNG 导出会重新渲染
类的标签画在 SVG 的 `<foreignObject>` 里,浏览器拒绝把它画到 canvas 上。本站的 PNG 导出过去会无声失败并退回成 SVG 文件;现在它会先用纯 SVG 文本标签重新渲染一遍。导出的 PNG 尺寸正确,只是字体排版与屏幕上略有差别。
泛型用波浪号,这带来一个副作用
`仓储~T~` 之所以用波浪号,是因为尖括号会和标签里的 HTML 冲突。副作用是:类名或成员名里真出现一个波浪号时,它会被当作类型参数的开头。少见,但真遇上会非常困惑。
什么时候该换一种图
如果你记录的是数据库而不是类型系统,请用 ER 图。这个区别很重要:类图建模的是表所没有的行为和继承,而 ER 图能正确表达键和多重性,这一点类图只能含糊带过。
如果整张图基本上就是一堆方框加 `-->`、没有任何成员,那你画的是架构图而不是类图。用带子图的流程图会更好看,也更不会引起误解。
还有,如果类的清单是从代码生成的,不妨想想图是不是也该生成。手工维护一张每周都在变的代码库的类图,一个月内必然过时——而一张错的图,代价比没有图更大。
其他图类型
作者 Dominik Malsch · 最后更新: