免费 · 无需注册 · 支持 .mmd 文件

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 · 最后更新:

打开编辑器 →