3878 字
约 12 分钟
6
S18-A01 · 限界上下文与上下文映射(战略 DDD)

适用读者:要决定"系统拆几个服务/几个模块"的架构师;被"微服务拆过头"或"单体太乱"两头夹击的技术负责人。
前置:S18-J06(战术建模)、S18-J05(度量)。分布式协作机制见 S17。
覆盖旧文:A-1534(超市案例与业务案例、用例图、领域事件、边界划分)、A-1533(战略设计段)、A-1536(问题碰撞段,4 点)——合计 69 点;另有 3 张 0 字未发布卡(A-1532 架构演进、A-1539 账户域、A-1540 交易域)按 missing_source 登记,提纲见 §4。
扩展点:E-S18-014(事件风暴产出物)、E-S18-015(上下文划分信号)、E-S18-016(上下文映射九种关系)。
上一篇:S18-J06 · DDD 战术建模:实体、值对象、聚合
下一篇:S18-A02 · DDD 工程结构与脚手架。


1. 一句话结论

限界上下文的边界不是"功能清单"划出来的,是"语言 + 团队 + 一致性要求 + 变更频率"四个信号交集划出来的;划完之后真正的工作量不在拆分,而在"上下文之间怎么协作"——这就是上下文映射。

A-1533 对战略设计的定义值得先立在这里:战略设计"以应对复杂的业务需求,通过抽象、分治的过程,合理拆分为独立的多个微服务,从而分而治之"。紧接着它给了评价标准:"与之评价拆分的是否合理,则是在需求开发上线时候,是否每次都大量操作多个微服务开发和上线。这样的战略设计是一种失败的微服务单体设计。"

这句话是本文最重要的一句判据:如果一个小需求上线要同时改 5 个服务、发 5 次版,你的拆分就是失败的——不管架构图画得多漂亮。原文引的是 Justin Etheredge 的《You're not actually building microservices》:"少数几个中等规模单体,周围环绕着一个服务生态系统,这更有意义"。

1.1 四个划分信号(E-S18-015)

信号 问法 划出去的迹象 不该划的迹象
语言 同一个词在两边含义相同吗? "订单"在履约侧指"履约单",在交易侧指"支付单" 两边都用同一张表、同一套字段名
团队 边界是否与团队边界重合? 两个团队为一个模块的所有权打架 一个 3 人团队维护 8 个服务
一致性 这些数据必须强一致吗? 可以接受"最终一致 + 补偿" 必须同一事务(→ 同一上下文/聚合)
变更频率 两边改动节奏差多少? 促销规则一周三改,账户体系一年一改 两边总是一起改

四个信号里,"语言"最难但最有效。DDD 的"统一语言"不是文档要求,是拆分工具:当你发现同一个词在两个子域里指两件事,那里就是一条边界(这也是 Evans 在《领域驱动设计》里"Ubiquitous Language"与"Bounded Context"两章连在一起讲的原因,见 BoundedContext )。

Conway 定律的现实含义:A-1536 引用它——"系统结构会与组织的沟通结构保持一致"。所以你无法设计出与团队结构矛盾的边界,只能调整其中之一。这一点决定了:拆分方案必须先和排班表对一遍。


2. 事件风暴:从便签到边界(E-S18-014)

A-1534 把建模过程讲得很具体,是可执行的:

步 动作 产出物 谁参加
1 画用例图(用户与系统交互的最简表示) 参与者、用例清单 产品 + 研发
2 穷举领域事件(过去时) 黄色便签墙 产品、测试、架构师一起
3 给事件补决策命令(用户动作) 蓝色便签 同上
4 给命令补领域对象(棕色便签) 领域对象清单 研发主导
5 标外部系统(粉色)、只读模型(绿色) 依赖清单 架构师
6 串业务流程(红色)把命令连到事件 流程链 全员
7 圈边界,再单独出领域划分图 限界上下文草图 架构师收敛

四色(实际是六色)便签的含义,A-1534 原文给全了:

  • 蓝色 决策命令:用户发起的行为动作(开始签到、开始抽奖、查看额度);
  • 黄色 领域事件:过去时态(签到完成、抽奖完成、奖品发放完成),描述"终态";
  • 粉色 外部系统:需要调外部接口才能完成的流程;
  • 红色 业务流程:串联命令到事件;简单场景可命令直连事件;
  • 绿色 只读模型:只读数据,不写库;
  • 棕色 领域对象:每个命令都对应一个领域对象。

一句话概括整张图左下角的语义:"一个用户,通过一个策略命令,使用领域对象,通过业务流程,完成 2 个领域事件,调用 1 次外部接口"——建模就是把这些节点找齐。

超市案例(原文的类比,用来给非技术同事解释):媳妇下达指令=决策命令;推着购物车进超市=带实体进入流程;装满的购物车=领域事件(任务完成);手里的烟=第二个领域事件(一次行为可完成多个事件);出门→地铁→采购→打车回家=领域编排。

产出物清单(E-S18-014):一场 2 小时的事件风暴,合格的产出是 5 样东西,缺一样就说明没做完:

  1. 领域事件清单(去重、过去时、按业务流排序);
  2. 命令—事件映射(每条命令能引发哪些事件);
  3. 领域对象清单(谁承载这条命令的状态);
  4. 外部系统清单(哪些流程要跨出去);
  5. 边界草图(圈出的上下文 + 每两个上下文之间的关系类型)。

第 5 项最容易被跳过,而它恰恰是战略设计的交付物。没有它,风暴会退化成"贴了一墙便签的头脑风暴"。

真实案例的规模感:A-1534 的营销抽奖需求包含活动配置、签到奖励、活动账户、抽奖策略(责任链 + 规则树)、库存扣减、阶梯抽奖共 9 条业务规则。这种复杂度才值得开一场风暴;一个 CRUD 后台不值得。


3. 上下文映射:九种关系(E-S18-016)

边界划完,接下来是"两边怎么协作"。Evans 在《领域驱动设计》第 14 章("Maintaining Model Integrity")给了这组模式,BoundedContext 条目也复述了它们:

关系 中文 语义 谁承担耦合 适用
Partnership 合作关系 两边同生共死,一起改一起发 双方 两个团队目标一致、节奏同步
Shared Kernel 共享内核 共享一小块模型与代码 双方(需变更评审) 模型高度重叠且稳定
Customer/Supplier 客户-供应商 下游有话语权,上游为其排期 上游 上下游关系明确、可协商
Conformist 顺从者 下游照抄上游模型,不改 下游 上游强势、下游无谈判力
Anticorruption Layer 防腐层 下游加一层翻译,隔离上游模型 下游 上游模型脏/易变(最常用)
Open Host Service 开放主机服务 上游提供稳定协议供多方使用 上游 一供多需(对外 API)
Published Language 发布语言 用共享的、文档化的语言(如 JSON Schema、行业标准) 双方 跨组织、跨版本
Separate Ways 各行其道 不集成,各做各的 无 集成成本 > 收益
Big Ball of Mud 大泥球 反模式:无边界 — 只作为"要逃离的对象"

三条实战判据:

  1. 默认选防腐层(ACL)。只要上游不是你控制的(第三方、遗留系统、另一个团队),就加翻译层。A-552 的"脆弱点隔离"就是同一思想在代码层的版本——把"会变的"关进一个小房间。
  2. Shared Kernel 是最高风险的关系。共享代码意味着共享变更节奏;只在"模型确实同一套且双方都能守评审"时用。多数团队的 Shared Kernel 最后变成"共享泥球"。
  3. Separate Ways 要主动选。很多"集成"其实是政治需求而非业务需求;能量化"集成成本 vs 收益"时,不集成往往是对的。

画法建议:上下文映射图不要画成"服务调用图"。服务调用图回答"谁调谁",上下文映射图回答"谁迁就谁的模型"。两者信息量完全不同:ACL 在调用图上是"多一跳",在映射图上是"下游承担翻译成本"。


4. 案例:账户域与交易域(补两张缺卡的提纲)

素材里 A-1539(DDD 架构场景-账户域)、A-1540(DDD 架构场景-交易域)未发布、正文 0 字,A-1532(DDD 架构演进)同样为空。三张卡都登记为 missing_source。按题名与原书大纲(A-1527 的"系统架构(5)"列出了 MVC / DDD / MVC2DDD / 账户域 / 交易域),这里给出可执行的提纲,推断内容明确标注,等原文补齐后替换。

4.1 账户域(推断提纲)

项 内容
聚合 Account(聚合根:账户号、状态、余额快照)、CreditLine(授信额度,独立聚合,用 accountId 引用)
不变式 余额不得为负;额度占用 ≤ 授信额度;状态迁移 开户→激活→冻结→销户 单向
领域事件 AccountOpened、CreditGranted、LimitAdjusted、AccountFrozen
与交易域的关系 交易域是下游(Customer),账户域提供 OHS + Published Language(账户/额度查询 API);交易域侧加 ACL 把"账户模型"翻成"下单可用额度"
关键取舍 余额"实时扣减"还是"额度预占 + 事后结算"——前者强一致、吞吐低;后者最终一致、需补偿

4.2 交易域(推断提纲)

项 内容
聚合 Order(根)、Payment(独立聚合,用 orderId 引用)、Refund
不变式 已支付订单不可改金额;退款总额 ≤ 支付总额;状态机 CREATED→PAID→SHIPPED→CLOSED
领域事件 OrderPlaced、OrderPaid、OrderShipped、Refunded
跨上下文协作 下单 → 账户域预占额度(同步,带超时);支付成功 → 发 OrderPaid(outbox)→ 履约域消费
关键取舍 支付回调幂等(见 A04 §4);订单状态机由谁驱动(交易域自己 vs 独立状态机服务,见 A04 §3)

为什么要给出"推断"而不是留空:因为一张 0 字卡如果不给提纲,它在验收表里就只是一行 missing_source,读者不知道缺什么。给出提纲 + 标注推断,等于把缺口变成待办。


5. 评审清单(战略设计)

一个拆分方案上评审会,用下面 12 条过一遍,每条都要给出具体证据,不接受"我觉得":

# 检查项 通过标准
1 每个上下文有没有自己的统一语言表? 至少 10 个词,且与其他上下文冲突的词已标注
2 边界与团队边界是否重合? 一个上下文 ≤ 1 个团队负责
3 跨上下文调用链最长几跳? 同步链 ≤ 3 跳,否则加异步/聚合服务
4 每个跨上下文关系是否标了类型? 九种之一,不能是"随便调"
5 是否默认加了 ACL? 上游非自控则必须
6 强一致需求是否已收敛在同一聚合? 同一事务内的写不跨聚合
7 一次需求上线要动几个服务? ≤ 2;>3 说明边界错
8 数据所有权是否唯一? 一张表只有一个上下文写
9 失败语义是否定义? 超时/重试/降级/补偿各有归属
10 演进路径是否写了? 有"先单体后拆分"或"绞杀者"路线(见 A03 §4)
11 是否有反腐蚀测试? ACL 有契约测试,上游改字段能被测出
12 拆分收益是否量化? 至少一项:发布频率、故障隔离、团队并行度

6. 常见误区

误区 真相
"一个上下文 = 一个微服务" 可以是一个模块、一个包、一个单体里的子系统;部署单元 ≠ 边界
"拆得越细越解耦" 拆过头的成本是"每次上线动 N 个服务",原文直接称之为失败的微服务单体
"上下文映射就是调用图" 映射图描述"谁迁就谁的模型",调用图描述"谁调谁"
"统一语言是写文档" 是建模工具:词义冲突处即边界
"共享内核能省事" 共享的是变更节奏,通常变成共享泥球
"事件风暴就是开会贴便签" 没有第 5 项产出(边界草图)的风暴等于没开

7. 自测

  1. 你系统里"订单"这个词,在几个团队口中含义不同?这就是几条边界?
  2. 一次需求上线要改几个服务?如果 >3,先改边界还是先改流程?
  3. 上游是第三方时,你选 Conformist 还是 ACL?给出成本对比。
  4. 为什么 Shared Kernel 的风险高于 Customer/Supplier?
  5. 事件风暴的第 5 项产出是什么?缺了会怎样?
  6. 你怎么向老板解释"不集成"(Separate Ways)是合理选择?

下一篇

S18-A02 · DDD 工程结构与脚手架

边界划完,代码怎么摆?下一篇讲六边形/洋葱/COLA 三个骨架的共同点、分层与 Maven 多模块的对应、依赖方向的可执行校验(ArchUnit 实测,含反向违规验证),以及脚手架(archetype)怎么落地。


本篇属于系列《S18 · 架构方法与领域建模》。旧文来源:A-1534、A-1533(战略段)、A-1536(问题碰撞段);A-1532/A-1539/A-1540 为 0 字未发布卡,登记 missing_source 并在 §4 给出推断提纲。上下文映射九关系口径出自 Evans《领域驱动设计》第 14 章与 Fowler bliki。

S18-A01 · 限界上下文与上下文映射(战略 DDD)
https://www.clxhxhhr.top/posts/5441/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。