上一篇文章中,我们学习了贫血模型和充血模型,解决了“业务规则应该放在哪里”的问题。
但一个系统里还有订单、商品、库存、支付等很多业务。哪些对象应该放在一起,边界在哪里,光靠充血模型回答不了。
这一篇我们继续学习 DDD 战略设计中三个基础概念:领域、子域和限界上下文。
一、为什么要划分业务边界
我们平时最熟悉的是技术分层:
controller service mapper entity这种分法解决的是代码放在哪里,却没有告诉我们业务应该怎么拆。
以电商系统为例,商品、订单、库存和物流都可能用到“商品”,但关注点完全不同:
- 商品中心关心标题、类目和上下架状态;
- 库存关心 SKU、可用库存和仓库;
- 订单关心下单时的名称、成交价格和数量;
- 物流关心重量、体积和配送要求。
如果所有模块共用一个Product,它很快就会变成谁都依赖、谁都不敢改的万能对象。
DDD 会先划分业务问题和模型的适用范围,再考虑代码、数据库和服务怎么落地。领域、子域和限界上下文,就是用来说明这些边界的。
二、领域与子域
领域对应Domain,简单来说,就是我们需要理解和解决的一块业务范围。
例如“电商”是一个领域,其中包含商品、订单、库存、支付和物流等业务问题。领域描述的是业务本身,不是 Java 包、数据库或技术框架。
领域的大小是相对的。从电商平台看,支付可能只是其中一部分;但对支付公司来说,支付本身又是一个大领域,还能继续拆成收单、退款、对账和清算。
一个大领域拆出的业务部分叫作子域。以电商领域为例,可以粗略拆成:
- 商品子域;
- 订单子域;
- 库存子域;
- 支付子域;
- 物流子域。
每个子域解决一类相对完整的问题。订单负责交易生命周期,库存负责增加、锁定、扣减和释放,物流负责发货和运输跟踪。
子域也可以继续拆,但不要为了套 DDD 概念无限拆分。拆分的目标是让职责更清楚;如果拆完每块只剩一两个增删改查方法,反而会增加协作成本。
三、核心域、支撑域和通用域
不同子域对业务的价值不同。DDD 通常把它们分为三类,用来判断有限的人力应该投入在哪里。
| 类型 | 说明 | 示例 |
|---|---|---|
| 核心域 | 体现主要业务竞争力,规则通常复杂且持续变化 | 交易平台的订单撮合 |
| 支撑域 | 支撑核心业务,并带有企业自己的规则 | 商家结算、售后审核 |
| 通用域 | 多数系统都会遇到,通常有成熟方案 | 认证、短信、文件存储 |
核心域不是固定的模块名单。同样是电商平台,零售平台可能重视交易和营销,物流平台可能重视履约调度,二手平台可能重视验货和担保交易。
判断时可以问三个问题:它是不是主要价值来源,规则是否复杂且经常变化,市面上是否已有成熟通用方案。
这里说“通用”,不代表它不重要。认证和权限同样重要,只是它们通常不能体现当前业务与竞争对手的差异,可以优先使用成熟产品或公共能力。
分类也不是常量。随着公司战略变化,支撑域可能变成核心域,自研能力也可能被公共平台替代。
四、限界上下文与通用语言
子域划分了业务问题,但还需要给模型规定明确的适用范围,这就是限界上下文(Bounded Context)。
在一个限界上下文内,业务术语有明确含义,模型和规则保持一致,并由明确的模块或团队维护。
“限界”限制的是模型生效的范围,不是限制业务沟通。一个上下文中的模型出了边界,不应该被默认复用,其他上下文要按照自己的业务重新表达。
还是以“商品”为例:商品上下文维护标题和上下架状态;库存上下文使用库存项表达 SKU 和库存数量;订单上下文保存下单时的商品快照;物流上下文只关心发货属性。
这意味着我们不需要一个万能Product。订单上下文可以定义自己的OrderItem:
publicclassOrderItem{/** 商品 SKU 标识,用于记录本次下单选择的具体规格 */privateLongskuId;/** 下单时的商品名称,商品改名不会影响历史订单 */privateStringproductName;/** 下单时的成交单价,用于保证订单金额可追溯 */privateBigDecimaldealPrice;/** 本次购买数量 */privateIntegerquantity;}它只保留订单需要的信息,不必带上商品详情、搜索关键词等字段。不同上下文需要协作时,通过接口或消息传递必要数据,而不是直接修改对方的表和对象。
这里还要注意:限界上下文不等于微服务。
限界上下文是业务语义和模型边界,微服务是部署和运行边界。单体项目也能通过模块和接口建立多个限界上下文;两个服务如果共用表、互改数据,业务边界仍然是混乱的。
限界上下文内部还需要通用语言(Ubiquitous Language)。它要求业务、产品、开发和测试对关键术语有一致理解。
例如“取消订单”和“超时关闭”可能是两个动作,前置条件和后续处理也不同,代码就应该直接表达:
order.cancelByUser();order.closeByTimeout();通用语言最终要落实到需求、类名、方法名、状态枚举、接口、事件和测试用例中,而不只是停留在会议里。
五、子域和限界上下文的关系
这两个概念很容易混淆,可以先记住:
- 子域是对业务问题空间的划分,回答“有哪些不同的业务问题”;
- 限界上下文是模型和解决方案生效的边界,回答“用哪套模型解决这些问题”。
理想情况下,一个子域通常对应一个限界上下文。但历史系统里,订单和库存可能还挤在同一个上下文;复杂的订单子域也可能拆成多个上下文。
这种对应关系还会受业务复杂度、团队职责和系统现状影响。划分的目标是得到清楚的业务边界,不是追求形式上的一一对应。
所以不能机械套用“一个子域等于一个限界上下文,更等于一个微服务”。真正需要检查的是:模型语义是否一致,规则是否有明确归属,边界之间是否通过清楚的契约协作。
六、从电商案例看边界如何落地
假设用户下单后,系统要锁定库存、处理支付并通知仓库发货。可以先按业务能力得到下面几个上下文:
| 限界上下文 | 主要模型 | 主要职责 |
|---|---|---|
| 商品上下文 | 商品、类目、SKU | 维护展示和销售属性 |
| 订单上下文 | 订单、订单项 | 创建交易并维护订单生命周期 |
| 库存上下文 | 库存项、锁定记录 | 管理可用、锁定、扣减和释放 |
| 支付上下文 | 支付单、退款单 | 处理支付和退款状态 |
| 物流上下文 | 发货单、物流轨迹 | 管理发货和运输过程 |
下单时,订单上下文获取必要的商品信息,库存上下文判断并锁定库存,订单保存成交快照,支付上下文处理支付,支付结果再推动订单和履约流程。
这里的重点不是一定要使用接口还是消息,而是每个上下文只维护自己的规则和数据。
对于已经运行多年的 MVC 项目,可以先做四件事:
- 查找同名但含义不同的业务词,例如各种
status和“完成”; - 核实哪些表被多个模块直接修改,并结合调用链、SQL 和日志确认真实用途;
- 找出经常一起变化的代码,判断它们是否属于同一业务边界;
- 先明确模块职责和跨边界接口,再决定是否拆库、拆服务。
老系统里可能存在补偿任务和历史兼容逻辑,不能看到跨模块更新就直接删除。先核实现状,再逐步收紧边界更稳妥。
业务边界也不能只由开发人员根据代码目录决定。产品、业务、开发和测试需要一起确认流程和术语,否则技术上看着整齐,模型却可能与真实业务不符。
七、常见误区和验证方法
划分边界时,常见误区主要有这些:
- 按数据库表划分子域;
- 把一个实体当成一个限界上下文;
- 用微服务数量证明业务边界合理;
- 为了减少字段重复,让所有模块共用一套 Entity;
- 只让开发人员根据代码目录划分业务边界。
边界划完后,可以检查:
- 每个子域解决的问题能否用一句话说清楚;
- 同一上下文内的关键术语是否只有一种含义;
- 每条核心规则是否有明确归属;
- 不同上下文是否直接修改对方数据;
- 跨边界传递的是明确契约,还是内部对象;
- 一个上下文变化时,是否经常要求其他上下文同步修改。
如果职责、术语和规则仍然说不清,边界就需要继续调整。领域知识本来就是逐步积累的,不必追求一次划到完美。
最后简单总结一下:
- 领域是一块需要理解和解决的业务范围,子域是对它的继续拆分;
- 核心域、支撑域和通用域体现不同的业务价值和投入优先级;
- 限界上下文规定业务语言和模型生效的范围;
- 子域属于问题空间,限界上下文属于模型和解决方案边界;
- 划分边界要看真实业务、数据归属和调用关系,不能直接套数据库表或微服务。
到这里,我们已经从“业务规则放在哪里”,走到了“哪些业务应该放在一起”。下一篇继续学习战略设计,看看这些边界怎么进一步指导系统拆分。