20.1 战略 DDD:子域、限界上下文与统一语言
领域驱动设计(DDD)首先是一种理解复杂业务、建立模型边界的协作方法,其次才是 Entity、Value Object、Aggregate 等代码模式。
如果团队没有与领域专家共同定义语言和边界,给类加上 AggregateRoot 注解不会自动产生领域模型。
领域与子域
领域是组织要解决的业务世界。子域把它拆成具有不同问题和能力的部分:
- Core Domain:形成竞争优势,值得投入最强建模能力;
- Supporting Subdomain:业务必要,但不是主要差异化来源;
- Generic Subdomain:行业通用能力,通常优先购买或复用成熟方案。
在赛事平台中,“匹配与赛制”可能是核心子域;通知是支持子域;身份认证可能采用通用产品。分类不是永久标签,要随战略变化重审。子域描述业务问题的划分,限界上下文描述某个模型的适用边界;两者是不同视角,不应先按组织图画好服务,再把每个框命名为子域。
统一语言进入代码和对话
统一语言(Ubiquitous Language)应同时出现在:
- 领域专家和开发者的日常讨论;
- 需求示例和验收标准;
- 类、方法、事件和错误名称;
- 监控、运营后台和支持手册。
例如“报名”“占位”“确认资格”“退赛”应有明确区别。若产品说“确认报名”,代码却叫 updateStatus(3),模型已经在翻译中丢失。
统一语言不是全公司的唯一词典。同一个词在不同业务语境可能有不同含义,这正是限界上下文存在的原因。
限界上下文保护模型含义
在“报名”上下文,Player 关心报名资格和种子排名;在“结算”上下文,AccountHolder 关心收款身份和风险状态。强迫两个上下文共享一个拥有几十个字段的 Player 类,只会形成共同耦合。
Registration Context Settlement Context
Player AccountHolder
- eligibility - paymentIdentity
- seed - riskStatus
- registrationStatus - balance限界上下文定义某套模型和语言有效的范围。它通常通过明确代码模块、数据所有权和公开契约维持边界,但不必一一对应微服务。一个模块化单体可以包含多个限界上下文,一个大型上下文也可能因运行原因包含多个部署单元。
从业务事件发现边界
可以围绕真实流程列出:
- 命令:谁想做什么;
- 领域事件:什么业务事实已经发生;
- 政策:看到某事实后决定什么;
- 不变量:任何时候都不能被破坏的规则;
- 外部系统和人工决策。
例如:
提交报名
→ 资格已验证
→ 名额已预留
→ 支付已授权
→ 报名已确认如果每一步由不同团队、语言和数据规则拥有,可能存在上下文边界;如果它们必须在同一原子规则中变化,过早拆开会制造分布式一致性问题。
Event Storming 等工作坊可以帮助发现这些线索,但彩色便签不是最终架构。结论还要经过代码依赖、数据所有权、组织责任和运行约束验证。
上下文映射描述关系
上下文之间不是简单箭头。常见关系包括:
- Partnership:双方共同规划和演进;
- Customer–Supplier:下游需求进入上游计划;
- Conformist:下游直接接受上游模型;
- Anti-Corruption Layer:下游翻译并保护自己的模型;
- Open Host Service + Published Language:上游提供定义清楚的服务协议,并用共享、文档化的交换语言描述数据;
- Separate Ways:集成价值不足,各自实现。
这里的“公开”是面向集成方的契约,不意味着必须对互联网匿名开放。共享一份交换 schema,也不等于共享双方的内部领域类。
同一个上游对不同下游可以使用不同关系。关系还应写明契约、SLA、升级和弃用责任。
边界判断问题
- 这组规则由同一批领域专家解释吗?
- 同一个词在两处是否具有不同生命周期或不变量?
- 数据由谁写,谁有权纠正?
- 哪些变化经常一起发生?
- 一个团队能否端到端拥有该能力?
- 强一致不变量是否被边界切开?
下一课进入单个上下文内部,使用实体、值对象和聚合表达业务不变量。
参考资料
- Martin Fowler, Bounded Context
- Martin Fowler, Ubiquitous Language
- Eric Evans, DDD Reference