UML 类图示例:为在线课程平台建模

以在线课程平台为例的 UML 类图:类、可见性、多重性,以及何时使用关联、组合、继承或接口。

· 创始人兼开发者

发布于

UML 类图示例:为在线课程平台建模

大多数 UML 图只活在教科书里,而类图是工程师至今仍在画的那一种:它用一张图描绘系统所关心的事物、每样事物持有什么、能做什么,以及它们之间如何关联。画得好的类图,能在任何人动手写迁移脚本或 API 之前,就结束关于领域模型的争论。

本文为一个小型在线课程平台绘制 UML 类图,每次只做一个决定,并在用到时讲解相应的表示法:可见性、多重性、关联、组合、继承和接口。最后介绍会让类图失去价值的常见错误,并简单看看图如何映射到代码。

什么时候该用类图

UML 2.5.1 规范把图分为结构图和行为图两类。类图属于结构图:它展示类型、类型的属性和操作,以及类型之间的关系,不包含任何时间概念。

你需要回答的问题 图
有哪些概念,它们知道什么,彼此如何关联 类图
数据如何存储:表、键、外键 ER 图
参与方之间按什么顺序发生了什么 时序图
一个对象会经历哪些状态 状态机图

类图适合用来画领域模型、库的公开类型,或者你即将实现的设计。它不适合用来罗列一个仓库里的所有类:IDE 在这方面做得更好,而且下一次提交之后图就过时了。

用两张表看懂表示法

类是一个最多有三个分栏的方框:名称、属性和操作。任何一个分栏都可以省略。

元素 表示法 含义
抽象类 名称用斜体,或写 {abstract} 不能实例化,由子类补全
接口 名称上方写 «interface» 一组操作的契约
可见性 + public、- private、# protected、~ package 谁可以使用该特性
属性 name: Type [multiplicity] = default 对象持有的数据
操作 name(param: Type): ReturnType 对象提供的行为
静态特性 加下划线 属于类本身,而不是某个实例

关系是类之间的连线,而大部分含义都由线的两端承载:

关系 线型 读作
关联 实线;空心箭头标出可导航的一端 “与……相关”
聚合 实线,整体一端为空心菱形 “聚集”;部分可以独立存在
组合 实线,整体一端为实心菱形 “由……组成”;部分随整体一同消失
泛化 实线,父类一端为空心三角形 “是一种……”
实现 虚线,接口一端为空心三角形 “实现”
依赖 虚线,目标一端为空心箭头 “使用”;目标的改动可能破坏源头

组合是最严格的关系。按照规范,一个部分对象在同一时刻最多属于一个组合体,组合体被删除时,它的部分也随之删除。共享聚合(空心菱形)则比较含糊:规范本身就说,它的确切语义因应用领域和建模者而异。只有当团队对它的含义达成一致时才使用;普通的关联能表达同样的意思,争议也更少。

多重性

多重性表示关联的一端可以有多少个对象:1 表示恰好一个,0..1 表示可选,*(与 0..* 相同)表示任意数量,1..* 表示至少一个,也可以写 2..5 这样的范围。把它写在被计数的那个类旁边的一端。Course 1 — 1..* Module 读作:“一门课程有一个或多个模块,每个模块恰好属于一门课程。”

漏写多重性,是类图引发争论而不是终结争论的最常见原因。下面的例子中,每个关联的两端都写明了多重性。

示例:在线课程平台

先从朴素的句子开始,而不是从方框开始:

  1. 讲师发布课程。一门课程有一位或多位讲师。
  2. 课程由有序的模块组成,模块由有序的课时组成。
  3. 课时可以是视频、测验或作业。
  4. 测验和作业需要评分,视频不需要。
  5. 学员选修课程。选课记录跟踪学习进度,并保存学员提交的作业。
  6. 学完一门课程的学员,会为这次选课获得一张证书。
  7. 课程按类别分组,一门课程可以属于多个类别。

用户:一个类,两个角色

第一反应往往是把 Student 和 Instructor 设计成 User 的子类。可一旦某位讲师去学别人的课程,这个设计就崩了:在大多数编程语言里,一个对象一生只有一个类,既不能同时是两者,之后也不能切换。应该把角色建模为关联:一个 User 讲授课程,一个 User 拥有选课记录。继承要留给那些终其一生都属于某一类的事物,比如测验是课时的一种。

生命周期绑定时使用组合

模块离开所属课程就没有意义,课时离开所属模块也一样。删除一门课程,两者都会被删除。这就是组合:在 Course 和 Module 一端画实心菱形,整体一端写 1。类别则不同。删除“数据工程”类别,不能把其中的课程也删掉,所以 Category 到 Course 是聚合,两端都是 *。

继承加接口

Lesson 是抽象类;VideoLesson、Quiz 和 Assignment 继承它的标题和位置。评分则横跨这棵继承树:测验和作业要评分,视频不用,而明年新增的同伴互评可能需要评分,却根本不是课时。这就是接口 Gradable,由 Quiz 和 Assignment 实现。GradingService 依赖 Gradable,而不是具体类,虚线依赖箭头表达的正是这一点。

带有自身数据的关联

Enrollment 位于 User 和 Course 之间。UML 为此提供了专门的表示法,即关联类,用一条虚线挂在关联线上。不过大多数团队会把它画成一个普通的类,带两条关联,因为它有自己的身份、数据和行为:选课日期、进度、提交的作业,以及最多一张证书。

Course 由 Module 组成,Module 又由 Lesson 组成。VideoLesson、Quiz 和 Assignment 继承抽象类 Lesson;Quiz 和 Assignment 实现 Gradable 接口。用户讲授课程,并通过 Enrollment 选课,Enrollment 持有 Submission 和一个可选的 Certificate。

32 个组件 · 16 个连接

在 Archboard 中打开
图表内容
  • Category
  • Course
  • User
  • Module
  • Enrollment
  • Certificate
  • Lesson
  • Submission
  • VideoLesson
  • Quiz
  • Assignment
  • Gradable
  • GradingService
  • groups
  • teaches
  • enrolls
  • for
  • earns
  • holds
  • answers
  • «use»
在线课程平台的 UML 类图:课程由模块和课时组成,三种课时类型位于同一个抽象类之下,可评分的作业隐藏在一个接口之后,选课记录把用户和课程连接起来。

这张图的读法如下:

  • 左上,课程目录。 Course 由 Module 组成,模块又由 Lesson 组成,实心菱形位于整体一端。Category 用空心菱形把课程归组。
  • 左下,课时类型。 Lesson 是抽象类。VideoLesson、Quiz 和 Assignment 用空心三角形指向它。
  • 右侧,学习。 一个 User 讲授课程,每门课程有 1..* 位讲师;一个用户可以有任意数量的选课记录,每条记录恰好对应一门课程。一个 Enrollment 保存提交的作业,最多获得一张 Certificate。
  • 右下,评分。 Quiz 和 Assignment 通过虚线和空心三角形实现 Gradable。每个 Submission 对应一个可评分项,GradingService 只依赖接口。

常见错误

  • 把代码库里的每个类都画上。 每张图十到十五个类是个不错的上限。更大的模型可以按领域拆分,或者把其余的类只画成不带分栏的名称。
  • 把 getter 和 setter 列为操作。 应该列出有领域含义的操作,比如 publish() 或 enroll(),而不是访问器。
  • 把表画成类。 像 courseId 这样的属性应该放在 ER 图里。在类图中,关联线本身就是引用。
  • 用继承表示角色。 如果一个对象可以获得或失去某个角色,那就是关联,而不是子类。
  • 没有含义的菱形。 如果没人说得清删除整体会对部分产生什么影响,就用普通关联。
  • 在领域层面暴露实现细节。 除非图本身就是讲这些,否则省略框架注解、DTO 和仓储类。

从图到代码

这张图可以直接映射到代码。用 TypeScript 写出来是这样:

interface Gradable {
  maxScore(): number;
  autoGrade(submission: Submission): Score | null;
}

abstract class Lesson {
  constructor(public title: string, public position: number) {}
}

class Quiz extends Lesson implements Gradable {
  questions: Question[] = [];
  maxScore(): number {
    return this.questions.length;
  }
  autoGrade(submission: Submission): Score | null {
    return submission.scoreAgainst(this.questions);
  }
}

class Course {
  private readonly modules: Module[] = []; // composition: owned and removed with the course
  categories: Category[] = []; // aggregation: categories exist on their own

  addModule(title: string): Module {
    const created = new Module(title, this.modules.length);
    this.modules.push(created);
    return created;
  }
}

组合变成了所有权:课程自己创建模块,课程消失后,没有任何东西会让模块继续存活。聚合则变成对一个拥有独立生命周期的对象的引用。代码和图不一致时,在同一个拉取请求里更新图;正如不会过时的架构图一文所说,当图是一个链接而不是截图时,这件事会容易得多。

在 Archboard 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 为每个概念添加一个 UML 节点包中的 类,属性和操作每行写一个。把 Lesson 的构造型设为 abstract。
  3. 为 Gradable 添加一个 接口,写上它的两个操作。
  4. 连接各个类。整体一端的箭头样式,组合设为 实心菱形,聚合设为 菱形;继承则在父类一端设为 三角。实现画成在接口一端带 三角 的虚线,依赖画成带 空心箭头 的虚线。
  5. 把多重性作为文本放在每一端旁边,并在连线标签里写上关联名称,比如“teaches”。
  6. 为设计决定添加一个 UML 注释,比如为什么把角色建模为关联。

这个示例共用 13 个图表节点,在 Free 套餐的限制之内。借助通过 MCP 连接的 AI 智能体,你可以描述需求,让它先起草这些类,再自己修正多重性。

常见问题

类图和对象图有什么区别? 类图展示类型以及类型之间的规则。对象图展示某一时刻的具体实例,包括它们的值和链接,适合用真实的例子来检验类图。

聚合和组合有什么区别? 在组合中,部分只属于一个整体,并随整体一起删除。在聚合中,部分可以独立存在,也可以被共享;它的确切含义由建模者决定,所以很多团队干脆改用普通关联。

类图必须和代码完全一致吗? 领域模型应当与概念和关系保持一致,而不是与每个字段和方法一致。把它保持在产品经理和工程师都能读懂的层次。

类图和 C4 组件图有什么不同? C4 组件图展示可部署的构建块及其职责。类图再往下深入一层,展示它们内部的类型;参见 C4 模型图示例。

可以从代码自动生成类图吗? 很多 IDE 都可以,但生成的结果会列出所有类,很难阅读。领域模型最好手工绘制,或借助 AI 智能体绘制,并只保留重要的类。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。