四、限界上下文(Bounded Context):如何识别和划分限界上下文
说实话,限界上下文这个概念,是 DDD 里最容易被误解、也最值得反复琢磨的一个。我见过太多团队,一上来就画个大而全的领域模型,结果代码里全是「万能对象」,改一个功能要动十几个服务。嗯,说白了,限界上下文就是帮你划清边界、避免这种混乱的。
4.1 什么是限界上下文?
先别急着背定义。我习惯这么理解:每个限界上下文,就是一个独立的「小世界」。在这个世界里,每个术语都有明确的含义,每个模型只服务于这个世界的业务逻辑。
举个例子。在电商系统里,「订单」这个词,在「下单上下文」和「物流上下文」里含义完全不同。下单时,订单包含商品、价格、优惠券;物流时,订单只关心地址、重量、配送状态。你想想看,如果强行用一个 Order 对象包打天下,那这个类迟早变成「上帝类」——什么都有,什么都改不动。
核心要点:限界上下文 = 模型边界 + 语言边界 + 实现边界。三者必须一致。
4.2 如何识别限界上下文?
我总结了三个实战中最好用的方法,每个都踩过坑,你直接拿去用。
4.2.1 从业务事件出发
别一开始就盯着数据库表。先问自己:这个系统里,发生了哪些关键业务事件?
- 用户提交了订单 → 触发「订单上下文」
- 仓库完成了拣货 → 触发「库存上下文」
- 财务确认了收款 → 触发「支付上下文」
每个事件,往往对应一个独立的上下文。我在项目中遇到过,团队把「订单创建」和「订单支付」混在一个上下文里,结果支付逻辑改了,订单查询也跟着崩。后来拆开,各自独立演进,清爽多了。
4.2.2 从术语歧义入手
这是个很妙的技巧。你让业务专家和开发坐在一起,把核心术语列出来。如果同一个词,不同人理解不一样——恭喜你,这里大概率需要拆成两个上下文。
| 术语 | 上下文A的理解 | 上下文B的理解 |
|---|---|---|
| 客户 | 下了单的人,有收货地址 | 注册了账号的人,有登录密码 |
| 商品 | 有库存、有价格 | 有图片、有详情描述 |
| 订单 | 待支付、待发货 | 已完成、已退款 |
你看,同一个词,在不同上下文里,属性、行为、生命周期都不一样。强行统一,只会让模型变得臃肿不堪。
4.2.3 从变化频率判断
这是我个人最常用的方法。问一个简单问题:这部分业务,多久改一次?
- 如果「商品详情页」每周都在调样式、改推荐算法 → 它应该独立
- 如果「用户登录」半年都不动一次 → 它也可以独立,但没必要拆太细
- 如果「订单状态机」经常因为促销活动加新状态 → 它必须独立
我曾经接手过一个项目,所有业务逻辑都写在一个服务里。商品展示改了,结果订单流程也跟着重新部署。你想想看,这多危险。后来按变化频率拆成三个上下文,各自独立发布,再也没出过这种问题。
4.3 划分限界上下文的实战原则
识别出来之后,怎么划?我给出四条铁律,每条都是血泪教训换来的。
4.3.1 一个上下文,一个数据库
这不是说物理上必须独立数据库,但逻辑上必须隔离。两个上下文不能直接操作对方的表。我见过最坑的写法:订单上下文直接去查库存表。结果库存改了字段名,订单查询直接报错。嗯,这种耦合,早晚出事。
// ❌ 错误示范:订单上下文直接查询库存表
public class OrderService {
public void createOrder(Order order) {
// 直接查库存表 —— 耦合!
int stock = jdbcTemplate.queryForObject(
"SELECT quantity FROM inventory WHERE product_id = ?",
Integer.class, order.getProductId()
);
}
}
// ✅ 正确做法:通过接口或事件通信
public class OrderService {
private final InventoryClient inventoryClient;
public void createOrder(Order order) {
// 调用库存上下文的接口
StockResult result = inventoryClient.checkStock(order.getProductId());
}
}
4.3.2 上下文之间,只通过接口通信
不管是 RPC、消息队列还是 REST,总之不能直接共享内部模型。每个上下文对外暴露的,应该是自己定义的「防腐层」。
我的习惯:每个上下文维护一个独立的 API 模块,里面只放对外暴露的 DTO 和接口定义。内部实现随便改,只要接口不变,其他上下文就不受影响。
4.3.3 上下文大小要适中
太小了,变成微服务灾难(一个服务就一个 CRUD 接口);太大了,又回到单体老路。我一般用「两个 pizza 团队」原则:一个上下文,应该可以由一个 5-9 人的团队独立维护。
4.3.4 允许上下文之间有「共享内核」
有些东西,比如通用的值对象(金额、地址、时间范围),可以在多个上下文里共享。但注意:共享的是定义,不是实现。我建议把这些放到一个独立的「公共库」里,所有上下文依赖它,但公共库本身几乎不变。
4.4 一个完整的识别案例
拿一个真实的「在线教育平台」来说。我参与过这个项目,当时我们是这样划分的:
- 课程上下文:管理课程基本信息、章节、课件。变化频率中等,主要受教研团队影响。
- 学习上下文:记录用户学习进度、完成情况、考试成绩。变化频率低,但数据量大。
- 支付上下文:处理订单、退款、发票。变化频率高,经常因为财务合规要求调整。
- 用户上下文:账号、权限、个人设置。变化频率极低,但安全性要求最高。
- 推荐上下文:根据用户行为推荐课程。变化频率极高,算法团队每周都在迭代。
每个上下文都有自己的数据库、自己的部署单元、自己的团队。你想想看,如果把这些全揉在一起,改推荐算法要重新部署整个平台,那得多痛苦。
注意:划分不是一次性的。随着业务发展,一个上下文可能会膨胀,这时候需要再次拆分。我见过一个「订单上下文」后来拆成了「下单上下文」「售后上下文」「对账上下文」三个。别怕重构,怕的是不敢动。
4.5 避坑指南
最后,分享几个我踩过的坑,你遇到了可以绕开走。
- 别把「微服务」和「限界上下文」划等号。一个上下文可以拆成多个微服务,也可以合并成一个。关键看团队规模和变化频率。
- 别过度设计。刚开始,3-5 个上下文就够了。等业务跑起来,再根据痛点逐步拆分。我见过一上来就拆 20 个上下文的项目,结果光服务间通信就把团队搞崩溃了。
- 别忘了「共享内核」的版本管理。公共库一旦改了,所有上下文都得跟着升级。我建议公共库的变更走严格的评审流程,最好半年才发一个新版本。
好了,限界上下文的识别和划分,说白了就是「找到业务边界,守住模型纯洁」。你按我说的三个方法去试,基本不会跑偏。下一章,我们聊聊上下文之间的映射关系——那才是真正考验架构设计能力的地方。