跳至正文
来两杯美式
返回

DDD 复杂业务系统设计(三):限界上下文,为什么“课程”在教务和市场是两种东西

By 来两杯美式
发布于

前言

上一篇我们把在线教育平台拆成了课程域、报名域、学习域等等。这一篇我们解决一个更细颗粒度的问题:一个域里的“课程”,到底应该用一套模型,还是几套模型?

答案是:几套。这件事不搞清楚,微服务拆完一定会乱。

一、先从一个生活问题开始

中文里“今天能穿多少穿多少”这句话,冬天和夏天意思完全相反。

脱离上下文,词语就是模糊的。

业务术语也是一样的

二、“课程”这个词,至少有三个意思

在在线教育 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:忽略上下文之间的集成

上下文之间一定会有交互。常见的集成方式:

不同的集成方式,对应不同的耦合度。下一篇讲实体和值对象的时候我会更细地展开。

总结

这一篇我们讲了:

下一篇我们进入战术设计的第一站——实体与值对象,看看一个“学员”或一个“地址”在代码里到底长什么样。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.DDD 复杂业务系统设计(一):为什么从“领域”开始,比从“数据库”开始更靠谱
  2. 02.DDD 复杂业务系统设计(二):领域与子域,把“学校/课程/学员”拆成三件事再说
  3. 03.DDD 复杂业务系统设计(三):限界上下文,为什么“课程”在教务和市场是两种东西
  4. 04.DDD 复杂业务系统设计(四):实体与值对象,把“学员”和“地址”放进代码的两种姿势
  5. 05.DDD 复杂业务系统设计(五):聚合与聚合根,一个“选课单”应该装多少东西
  6. 06.DDD 复杂业务系统设计(六):领域事件,让“作业提交”自动触发批改进度
  7. 07.DDD 复杂业务系统设计(七):分层架构,你的“业务代码”应该待在哪一层
  8. 08.DDD 复杂业务系统设计(八):整洁架构 vs 六边形架构 vs DDD 分层,我选哪一种
  9. 09.DDD 复杂业务系统设计(九):中台,在线教育平台到底应该共享什么
  10. 10.DDD 复杂业务系统设计(十):从战略到微服务,一张图跑通 DDD+中台+微服务
  11. 11.DDD 复杂业务系统设计(总结):从领域到微服务,一张图看懂 DDD 落地全流程

上一篇
软件设计原则:SOLID + KISS / YAGNI / LOD 全景
下一篇
DDD 复杂业务系统设计(二):领域与子域,把“学校/课程/学员”拆成三件事再说