前言
上一篇我们把在线教育平台拆成了课程域、报名域、学习域等等。这一篇我们解决一个更细颗粒度的问题:一个域里的“课程”,到底应该用一套模型,还是几套模型?
答案是:几套。这件事不搞清楚,微服务拆完一定会乱。
一、先从一个生活问题开始
中文里“今天能穿多少穿多少”这句话,冬天和夏天意思完全相反。
脱离上下文,词语就是模糊的。
业务术语也是一样的。
二、“课程”这个词,至少有三个意思
在在线教育 SaaS 里,“课程”这同一个词,在不同角色眼里是不同的东西:
教务人员眼中的课程
- 关注:章节、课时、知识点、配套资料、难度等级
- 关心:课程结构是否清晰、知识点是否完整
- 用一个“课程内容模型”就够了
市场人员眼中的课程
- 关注:标题、封面、卖点、价格、促销、销量
- 关心:怎么卖、怎么投放、转化率
- 用一个“课程营销模型”就够了
学员眼中的课程
- 关注:是否已报名、学习进度、下一步该学什么
- 关心:自己学到哪了、还差多少
- 用一个“课程学习模型”就够了
这三个“课程”如果强行用同一套数据模型来表达,会出现什么情况?
- 表字段臃肿(既要存知识点、又要存营销信息)
- 角色权限混乱(教务能改营销字段)
- 改一处影响全局
- 微服务拆不开
三、限界上下文:术语的边界
DDD 给出了一个专门的概念来解决这个问题——限界上下文(Bounded Context)。
定义:限界上下文是一个明确的边界,边界内有一套统一的术语和模型。
关键词:
- 限界:明确边界,边界外的事情不归我管。
- 上下文:在这个边界内,术语有唯一含义。
在在线教育 SaaS 里,我们可以这样划分限界上下文:
在线教育平台
├── 课程内容上下文(教务视角)
│ 术语:课程、章节、课时、知识点
│ 模型:Course(Syllabus)、Chapter、Lesson
│
├── 课程营销上下文(市场视角)
│ 术语:课程、卖点、促销、销量
│ 模型:Course(MarketingInfo)、Promotion
│
├── 课程学习上下文(学员视角)
│ 术语:课程、进度、笔记、下一步
│ 模型:CourseEnrollment、Progress
│
└── ... 其他上下文
“课程”这个词在三个上下文里都是合理的,但意思完全不同。这就是限界上下文的精髓——同一个词,不同的上下文有不同的模型,互不干扰。
四、通用语言:上下文内的“普通话”
有了限界上下文,还需要一个配套机制——通用语言(Ubiquitous Language)。
定义:通用语言是在同一个限界上下文内,团队所有角色(产品、设计、开发、测试、运营)共同使用的、唯一的一套术语。
它的核心要求是:同一个词在同一个上下文里,意思必须完全一致。
举例:
- 课程内容上下文里,“课程” = 一组结构化的教学内容(包含章节、课时)。
- 课程营销上下文里,“课程” = 一个上架销售的商品(包含价格、卖点)。
如果产品说“课程”,开发要立刻知道是哪个上下文里的课程——这就是通用语言的价值。
通用语言怎么落地?
实际项目里,我一般用这几个办法:
1. 术语表
为每个上下文维护一个术语表,列出所有核心概念、定义、示例。所有人必须按术语表说话。
2. 领域事件命名
领域事件名称本身就是通用语言的一部分。比如 CoursePublished(课程已发布)这个事件,发布方、订阅方对“已发布”的理解必须一致。
3. 文档与代码一致
通用语言中的名词 → 实体类名。 通用语言中的动词 → 领域方法名或领域事件名。
// 通用语言:课程内容上下文中,"发布课程" 是一个明确动作
class CourseService {
void publish(Course course) { /* ... */ }
}
// 通用语言:营销上下文中,"上架课程" 是另一个动作
class MarketingService {
void listOnShelf(CourseMarketing course) { /* ... */ }
}
4. 团队共识
通用语言不是文档定出来的,是在事件风暴、需求评审、代码评审中不断对齐出来的。
五、限界上下文和子域是什么关系?
很多人会混淆这两个概念。我用一个表格说清楚:
| 维度 | 子域 | 限界上下文 |
|---|---|---|
| 是什么 | 业务问题的划分 | 模型和代码的边界 |
| 颗粒度 | 较粗(按业务阶段) | 较细(按模型一致性) |
| 关注点 | “我们要解决哪些问题” | “每个问题用什么模型解” |
| 划分依据 | 业务阶段/功能模块 | 团队共识/模型差异 |
| 谁来分 | 领域专家主导 | 架构师+开发共同参与 |
关系:子域包含一个或多个限界上下文。一个子域如果内部模型非常复杂,可能拆成多个上下文;如果简单,一个上下文就够。
子域(业务问题)
└── 限界上下文 1(一个模型)
└── 限界上下文 2(另一个模型)
└── ...
以在线教育为例:
- 课程域(子域)= 一个限界上下文还是多个?多个。因为教务、学员、市场视角不同。
- 认证域(子域)= 一个限界上下文。内部模型简单,不需要拆。
六、限界上下文直接决定微服务边界
这是最关键的一点:限界上下文 ≈ 微服务边界。
| 限界上下文 | 微服务 |
|---|---|
| 课程内容上下文 | course-content-service |
| 课程营销上下文 | course-marketing-service |
| 课程学习上下文 | learning-service |
| 用户认证上下文 | auth-service |
每个限界上下文:
- 独立的代码库
- 独立的数据库
- 独立的部署
- 独立的团队(康威定律:组织结构决定系统结构)
这意味着:限界上下文划分错了,微服务就拆错了。后面所有的问题——服务调用乱、数据不一致、发布互相影响——根源都在这里。
七、怎么划分限界上下文?
实操中我一般用三步法:
步骤 1:找“同一个词”
把所有业务术语列出来,找同一个词在不同地方意思不一样的情况。出现这种歧义的地方,几乎一定需要拆分上下文。
步骤 2:找“模型冲突”
当一个领域对象在不同场景下需要存不同的属性、用不同的方法,就说明需要拆。
// ❌ 错误:一个大而全的 Course
class Course {
String title; // 营销用
BigDecimal price; // 营销用
List<Chapter> syllabus; // 教务用
int progress; // 学员用
void publish(); // 教务用
void promote(); // 市场用
void startLearning(); // 学员用
}
// ✅ 正确:按上下文拆分
// 课程内容上下文
class CourseContent { String title; List<Chapter> syllabus; }
// 课程营销上下文
class CourseMarketing { String title; BigDecimal price; }
// 课程学习上下文
class CourseProgress { long userId; long courseId; int progress; }
步骤 3:找“团队边界”
如果两个团队经常因为同一段代码互相吵架,那大概率是上下文没分对。康威定律告诉我们:组织结构决定系统结构。
八、常见误区
误区 1:上下文越多越好
上下文越多,集成成本越高。一个 5 人的小项目,硬拆 10 个上下文是灾难。
原则:能在一个上下文内搞定的,就别拆。只在该拆的时候拆。
误区 2:上下文一旦划定就永远不变
业务在演进,上下文也要演进。每 6-12 个月回顾一次上下文划分,看是否需要调整。
误区 3:忽略上下文之间的集成
上下文之间一定会有交互。常见的集成方式:
- 防腐层(ACL):新旧系统或外部服务之间
- 领域事件:异步解耦
- API 调用:实时同步
- 数据同步:通过消息队列异步同步数据
不同的集成方式,对应不同的耦合度。下一篇讲实体和值对象的时候我会更细地展开。
总结
这一篇我们讲了:
- 同一个词在不同上下文里意思不同。
- 限界上下文 = 模型的边界。
- 通用语言 = 上下文内的统一术语。
- 限界上下文 ≈ 微服务边界。
下一篇我们进入战术设计的第一站——实体与值对象,看看一个“学员”或一个“地址”在代码里到底长什么样。