返回模板页

UML 类图示例

下面这些案例展示了不同开发团队如何在各类系统中建模领域逻辑、继承层次和服务边界。参考这些模式,可以跨语言和框架复用。

UML 类图示例

真实案例

用户认证系统

使用场景: 后端开发者,设计认证服务

User { id: UUID, email: string, passwordHash: string }
+ login(email, password): Token
+ logout(token): void
+ resetPassword(email): void
Token { value: string, expiresAt: DateTime, userId: UUID }
Role { name: string, permissions: string[] }
User *── * Role(多对多)

这样组织的原因: 在实现之前建模认证,会提前暴露设计问题:Token 应该是值对象还是实体?Role 和 Permission 应该分开吗?画图强迫在写代码前做出这些决定。

内容管理系统

使用场景: 全栈开发者,构建博客平台

Article { id, title, body, status: Draft|Published }
+ publish(): void + archive(): void
Author { id, name, bio } ──1 Article
Tag { name } *── * Article
Comment { body, createdAt } *── 1 Article
MediaAsset { url, mimeType } *── * Article

这样组织的原因: Article上的Status枚举和与Tag的多对多关系,推动了用关联表而非JSON列的决策——这个权衡在没有图的情况下很容易被忽略。

支付处理领域

使用场景: 平台工程师,设计支付抽象层

PaymentMethod(抽象){ id, userId }
← CreditCard { last4, expiry, token }
← BankAccount { accountNumber, routingNumber }
Payment { id, amount, currency, status }
+ process(): Result + refund(amount): void
Payment *── 1 PaymentMethod
Refund { id, amount, reason } *── 1 Payment

这样组织的原因: 抽象的PaymentMethod类和具体子类清晰展示了策略模式。评审者不需要读代码就能看懂多态设计意图。

库存管理系统

使用场景: 开发者,设计仓库管理后端

Product { sku, name, description }
InventoryItem { quantity, warehouseId, reservedQty }
1 ── * Product
StockMovement { type: In|Out|Transfer, quantity, timestamp }
*── 1 InventoryItem
PurchaseOrder { status, expectedDelivery }
1 ── * PurchaseOrderLine { product, quantity, unitCost }

这样组织的原因: StockMovement作为不可变事件日志(而非直接更新数量)是一个关键的架构决策,图让整个团队都清晰看到了这个选择。

通知服务

使用场景: 工程师,设计多渠道通知系统

Notification(抽象){ id, userId, createdAt, + send() }
← EmailNotification { subject, htmlBody, recipient }
← PushNotification { title, body, deviceToken }
← SMSNotification { phone, message }
NotificationPreference { userId, channel, enabled }
NotificationTemplate { name, subject, body }

这样组织的原因: 抽象基类强制了统一的send()接口。新增渠道意味着继承Notification——图向未来的贡献者传达了这个扩展点。

微服务领域边界

使用场景: 架构师,为大型电商平台定义服务边界

OrderService: Order, OrderItem, OrderStatus
CatalogService: Product, Category, PriceRule
UserService: User, Address, PaymentMethod
FulfillmentService: Shipment, Tracking, Warehouse
─── 跨服务引用只传ID,不传对象引用

这样组织的原因: 用类图展示领域边界(而非基础设施)帮助团队执行服务间通过ID和事件通信的原则,而不是共享对象引用。

画好 UML 类图的技巧

  • 类图聚焦于一个限界上下文或领域区域——试图在一张图里展示整个系统只会产生无法阅读的混乱。
  • 优先展示最重要的关系,而不是力求完整——一张有30个类和50条箭头的图什么都传达不了。
  • 一致使用可见性修饰符(+/-/#)来展示类的预期公共API,而不只是内部字段。
  • 类图是设计工具,不是真理之源。只为架构仍在积极演进的区域持续维护它。

相关资源

和相似工具的对比

类图 vs ER 图

ER 图描述数据怎么存:表、字段、外键。类图描述行为怎么组织:方法、继承、接口。在简单的 CRUD 项目里两者长得很像,导致很多人拿一种图当两种用。一个判断标准:如果你的图上没有任何方法,那它其实是 ER 图,只是贴了 UML 的标签。

类图 vs 时序图

类图是静态结构快照,表达「有哪些类型、彼此什么关系」;时序图表达单个场景随时间展开的调用过程。结构问题(这段逻辑该放哪个类)看类图,流程问题(为什么这个调用发生了两次)看时序图。

手绘类图 vs 从代码自动生成

从源码逆向生成的类图会把每个类、每个字段都画出来,看着完整但基本没法读——重要关系被噪音淹没了。手绘类图的价值恰恰在于「你主动决定省略什么」。自动生成适合当索引查阅,手绘适合讲解和设计评审。

完整 UML 规范 vs 简化方框

严格 UML 对聚合、组合、依赖、实现有各自的符号。现实是大多数团队记不住哪个菱形是哪个意思,用错了反而传递错误信息。除非是在写正式规格说明书,用普通方框加带文字标注的箭头,沟通反而更可靠。

常见误区

  • 把所有字段和 getter 都画上去

    一个类挂 20 个属性加 20 个访问器,占掉半张图却几乎没传递信息。只画和你正在解释的那些关系相关的字段。类图是一个关于设计的论证,不是代码清单。

  • 该用组合的地方画成了继承

    两个类只是共享几个字段,就在它们之间画继承箭头,等于把一个错误设计固化进了图里——而看图的人真的会照着实现。判断方法:问「子类在任何场景下都『是』父类吗」。如果它只是『有』父类的数据,那就是组合。

  • 关系不标基数、不标方向

    Order 和 Customer 之间画一条光秃秃的线,几乎什么都没说:一个订单对应一个客户还是多个?Customer 知道 Order 的存在吗?请补上基数(1、0..1、*)和方向;不标的话读者会自己猜,而且通常猜错。

  • 画的是数据库而不是领域模型

    中间关联表、自增主键这些是存储层的产物,不是领域概念。把它们画进类图,会让设计讨论被现有表结构绑死——而现有表结构恰恰是设计评审时最该被质疑的东西。

常见问题

一张类图画多少个类比较合适?+

用于阅读和讨论的图,大致 5~15 个类。超过这个量就按限界上下文或子系统拆分:每个内聚区域一张图,再加一张高层图说明这些区域怎么连接。一张 60 个类的图在技术上是准确的,在实践中是没人看的。

方法要写全参数和返回值吗?+

只在签名本身就是重点时才写全。比如你要解释一个策略接口,签名就是核心信息,必须写。而对于只是被引用一下的类,写个类名就够了。到处写完整签名会让图体积翻三倍,信息量却增加很少。

一定要严格遵守 UML 规范吗?+

看读者是谁。课程作业或正式规格文档:要,规范本身就是评分项。团队内部设计讨论:简化方框 + 文字标注的箭头效果更好,因为多数读者本来就分不清 UML 那四种关系菱形的区别。

接口和抽象类怎么画?+

用明确的标签(«interface»、«abstract»)或斜体之类的视觉约定标出来,并把实现类放在抽象的下方。抽象之所以必须画在图上,是因为它定义了系统的扩展点——而这通常正是读者最想从设计里获得的信息。

函数式语言或 Go 这类代码还适合画类图吗?+

适合,只是读法要变。方框对应 struct、模块或类型;继承箭头基本消失,取而代之的是接口满足关系和组合。类图的核心价值——说明有哪些类型、彼此怎么依赖——在没有经典 OOP 继承的语言里依然成立。

在线开始编辑

回到模板页,替换成你自己的内容,就可以继续使用这套结构。

使用这个模板: /editor/new?template=class-diagram

使用这个类图模板