领域建模入门:什么是领域模型?
说到领域模型,我脑子里第一个蹦出来的画面,是十年前一个电商项目的白板。那时候我们团队七八个人,围着白板画了一整天,就为了搞清楚「订单」和「商品」到底怎么个关系。画到最后,项目经理说:「这不就是几张图吗?能跑代码吗?」
嗯,能跑。而且跑得稳不稳,就看这几张图画得对不对。
领域模型,说白了就是用图形化的方式,把业务世界的概念和规则翻译给技术世界看。它不是数据库表设计,也不是类图大全,而是业务人员、架构师、开发之间那个「哦,原来你说的是这个」的共同语言。
为什么需要领域模型?
我见过太多项目,需求文档写了几十页,开发一读:「这不就是CRUD吗?」结果上线后,业务方说「不对,我们说的退款不是这个意思」。为什么会这样?因为文字有歧义,而图没有。
领域模型的价值,我总结了三句话:
- 消除歧义——一张图胜过千言万语,业务方指着图说「这个箭头不对」,比看文字文档快十倍。
- 发现遗漏——画关系的时候,你总会发现「咦,这个对象好像少了个属性」或者「这两个实体之间是不是还缺个关联」。
- 指导设计——模型画清楚了,代码的包结构、数据库表设计、服务拆分,基本就定了七八成。
UML类图:领域模型的通用语言
UML类图是画领域模型最常用的工具。你不需要记住所有UML符号,掌握下面这几个就够了:
| 元素 | 符号 | 含义 |
|---|---|---|
| 类(Class) | 矩形框,分三格:类名、属性、方法 | 表示一个业务概念 |
| 关联(Association) | 实线箭头 | 表示两个类之间有联系 |
| 多重性(Multiplicity) | 1, 0..*, 1..* 等 | 表示关联的数量关系 |
| 聚合/组合 | 空心/实心菱形箭头 | 表示整体-部分关系 |
你想想看,一个电商系统里,「用户」和「订单」之间是什么关系?一个用户可以有多个订单,一个订单只属于一个用户。这就是典型的一对多关系。用UML画出来,就是两个矩形框之间连一条线,线上标上「1」和「0..*」。
实体识别:从需求中挖出核心概念
实体识别是领域建模的第一步。怎么做?我有个笨办法,但很管用:把需求文档里的名词全部圈出来。
举个例子,假设我们要做一个在线课程系统,需求里有这么一段话:
「用户可以购买课程,每个课程包含多个章节,每个章节下有多个视频。用户可以给课程写评价,一个课程可以有多个评价。」
圈出名词:用户、课程、章节、视频、评价。这些就是候选实体。
然后问三个问题:
- 这个名词有独立存在的意义吗?——「视频」不能脱离「章节」存在,但它本身有属性(时长、大小、播放地址),所以是实体。
- 这个名词有属性吗?——「用户」有用户名、邮箱、注册时间,有属性就是实体。
- 这个名词需要被追踪吗?——「评价」需要知道是谁写的、什么时候写的,需要追踪,所以是实体。
关系建模:三种核心关系
实体找出来了,接下来就是画它们之间的线。UML类图里,关系主要分三种:
1. 一对一(1:1)
一个实体A对应且仅对应一个实体B。比如:一个用户对应一个身份证信息。这种关系在实际业务中相对少见,但一旦出现,往往意味着两个实体其实可以合并,或者有特殊的业务约束。
// 代码示例:一对一关系
public class User {
private Long id;
private String username;
private IdCard idCard; // 一个用户只有一个身份证
}
public class IdCard {
private Long id;
private String idNumber;
private User user; // 一个身份证只属于一个用户
}
2. 一对多(1:N)
这是最常见的业务关系。一个用户有多个订单,一个课程有多个章节,一个部门有多个员工。在UML类图中,我们在关联线的「多」端标上 0..* 或 1..*。
// 代码示例:一对多关系
public class Course {
private Long id;
private String title;
private List<Chapter> chapters; // 一个课程有多个章节
}
public class Chapter {
private Long id;
private String name;
private Course course; // 每个章节属于一个课程
}
3. 多对多(M:N)
一个学生可以选多门课,一门课可以有多个学生。这就是多对多。在UML类图中,关联线两端都标上 0..*。但在代码实现时,通常需要引入一个中间表或中间实体。
// 代码示例:多对多关系(通过中间实体)
public class Student {
private Long id;
private String name;
private List<Enrollment> enrollments; // 一个学生有多个选课记录
}
public class Course {
private Long id;
private String title;
private List<Enrollment> enrollments; // 一门课有多个选课记录
}
// 中间实体:选课记录
public class Enrollment {
private Long id;
private Student student;
private Course course;
private LocalDateTime enrollTime; // 选课时间,这是多对多关系特有的属性
}
实战:在线课程系统领域模型
好了,理论说完了,我们动手画一个完整的领域模型。还是那个在线课程系统,我们识别出以下实体:
- User(用户):用户名、邮箱、角色
- Course(课程):标题、描述、价格
- Chapter(章节):名称、排序号
- Video(视频):标题、时长、播放地址
- Review(评价):评分、内容、创建时间
- Enrollment(选课记录):选课时间、学习进度
它们之间的关系:
- User 1 —— 0..* Enrollment(一个用户有多个选课记录)
- Course 1 —— 0..* Enrollment(一门课有多个选课记录)
- Course 1 —— 0..* Chapter(一门课有多个章节)
- Chapter 1 —— 0..* Video(一个章节有多个视频)
- User 1 —— 0..* Review(一个用户有多个评价)
- Course 1 —— 0..* Review(一门课有多个评价)
下面我用SVG把这张图画出来:
建模过程中的常见误区
画了这么多年模型,我总结出几个新手最容易掉进去的坑:
- 把数据库表设计当成领域模型——领域模型关注业务概念,不是物理存储。外键、索引这些东西不要出现在领域模型里。
- 过度建模——一个简单的博客系统,非要画出十几个实体,连「点赞记录」都单独成一个实体。记住:模型是沟通工具,不是艺术品。
- 忽略业务规则——比如「一个用户只能同时拥有一个进行中的课程」,这种约束在模型上怎么体现?可以在关联线上加约束说明,或者在实体里加状态字段。
- 关系方向搞反——一对多关系里,「一」端放哪里,「多」端放哪里?我的经验是:从「主」到「从」。用户是主,订单是从,所以用户那边标1,订单那边标0..*。
小结:领域模型的核心要点
领域模型不是什么高深莫测的东西。它就是一张图,一张让业务和技术能坐在一起聊天的图。记住三个要点:
- 实体从名词中来——圈出需求文档里的名词,问三个问题,实体就出来了。
- 关系从业务中来——1:1、1:N、M:N,每种关系都有对应的业务场景,不要凭空想象。
- 模型从沟通中来——画完模型,一定要找业务方确认。模型错了,后面的代码全是白写。
我个人习惯,每次画完领域模型,会打印出来贴在工位上。开发过程中遇到任何疑问,先看模型,模型上没有的,再去找业务方确认。这比翻几十页的需求文档快多了。