DDD 实战(二):战略设计,从业务语言到限界上下文
DDD 实战(二):战略设计,从业务语言到限界上下文
推荐 BeWild 代充 Codex ,工作猛猛提效。
DDD 的战略设计,解决的是大系统的结构问题。
它不急着问类怎么写、表怎么建、Repository 放在哪里,而是先问几个更上游的问题:
- 这个业务到底由哪些领域构成?
- 哪些能力决定产品竞争力?
- 哪些能力只是支撑主业务运行?
- 哪些能力可以复用成熟方案?
- 哪些概念必须使用同一套模型?
- 哪些概念虽然名字一样,但在不同语境下含义不同?
这些问题没有回答清楚,后面的战术设计和代码分层都会变形。
1. 领域:业务问题的边界
领域可以先粗略理解为“系统要解决的业务问题空间”。
电商有交易领域、支付领域、履约领域、库存领域、营销领域。支付系统有收银台、支付单、渠道路由、清结算、账务、风控。协同办公 IM 有账号、组织、关系、群组、消息、通知、工作台、审批、开放平台、搜索、安全治理。
注意,领域不是技术模块。
“MySQL”“Redis”“MQ”“HTTP 网关”不是领域,它们是实现手段。“用户表”“消息表”“群表”也不是天然领域,它们只是某个模型的持久化形态。
领域划分应该先从业务能力出发,再落到系统边界。Microsoft 的领域分析文档也强调,微服务边界应围绕业务能力,而不是围绕数据访问、消息等水平技术层。Microsoft: Domain analysis for microservices
2. 子域:不是所有业务都同样重要
一个大领域可以继续拆成多个子域。DDD 通常把子域分成三类:核心域、支撑域、通用域。
核心域 是产品差异化和竞争力所在。它值得投入最强的建模精力,不能轻易外包给通用方案。
支撑域 是业务运行必需,但不是核心竞争力。它通常需要定制开发,但建模深度和投入优先级低于核心域。
通用域 是行业里已经有成熟解法的能力。能买就买,能复用就复用,不要把团队最宝贵的注意力耗在这里。
以协同办公 IM 为例,如果我们把它定位成“企业沟通协作平台”,那核心域可能包括:
- 会话与消息。
- 组织身份与通讯录。
- 群组协作。
- 开放平台生态。
- 安全治理和权限体系。
支撑域可能包括:
- 通知。
- 搜索。
- 日程。
- 审批。
- 工作台应用入口。
通用域可能包括:
- 短信验证码。
- 基础文件预览。
- 通用对象存储。
- 第三方登录协议适配。
这个分类不是永久不变的。如果产品战略变了,子域分类也会变。比如一个普通 IM 产品里,审批可能只是支撑域;但如果产品转向深度 OA 协同,审批和流程编排就可能上升为核心域。
3. 统一语言:模型先进入语言,再进入代码
DDD 非常重视统一语言。所谓统一语言,不是写一份术语表放在文档里,而是让业务、产品、开发、测试在同一个上下文里使用同一套词,并把这套词落实到代码、接口、事件、表名和文档中。
协同办公 IM 里,“用户”这个词就应该被拆开看。
在账号上下文里,我们可能说:
- Account:一个可登录账号。
- Credential:账号凭证。
- Device:登录设备。
- LoginSession:登录会话。
在组织上下文里,我们说:
- Tenant:企业或租户。
- Department:部门。
- Member:组织成员。
- OrgRole:组织角色。
在关系上下文里,我们说:
- ContactRelation:联系人关系。
- FriendRequest:好友申请。
- BlockRelation:拉黑关系。
- ExternalContact:外部联系人。
在群组上下文里,我们说:
- Group:群。
- GroupMember:群成员。
- GroupRole:群角色。
- GroupPolicy:群治理规则。
这些词背后都是“人”,但它们不是同一个模型。强行复用一个 User,会让语言变模糊,也会让代码变脆。
4. 限界上下文:模型有效的范围
限界上下文是战略设计里最关键的概念之一。
Martin Fowler 对 Bounded Context 的解释是:一个模型只在特定上下文内保持一致和明确,不同上下文之间需要显式映射。Martin Fowler: Bounded Context
换句话说,限界上下文回答的是:这套模型在哪个范围内成立?
比如“成员”在组织上下文里,关注的是企业、部门、岗位、入职状态、组织角色、字段可见性。在群组上下文里,关注的是群昵称、群角色、入群方式、禁言状态、最后阅读位置。它们都可以关联到同一个现实中的人,但模型关注点完全不同。
如果两个上下文都叫 Member,也可以接受,只要它们处在不同包、不同服务或不同模块边界内,并且交互时使用明确的防腐层或 DTO。问题不在于重名,而在于没有边界。
5. 上下文映射:边界之间怎么协作
限界上下文不是孤岛。复杂系统的关键恰恰在于上下文之间如何协作。
DDD 里常见的上下文关系包括:
- 客户方/供应方:一个上下文依赖另一个上下文提供能力。
- 开放主机服务:上游上下文用稳定 API 暴露能力。
- 发布语言:上下文之间使用约定好的接口、事件或协议沟通。
- 防腐层:下游上下文通过转换层隔离上游模型。
- 各行其是:两个上下文没有必要集成,保持独立演进。
协同办公 IM 中,组织上下文和消息上下文就不应该共享组织表。更合理的方式是:
- 组织上下文负责组织成员事实。
- 组织上下文发布
OrganizationMemberChanged事件。 - 消息上下文维护自己需要的成员快照或权限缓存。
- 消息发送时只依赖消息上下文内的发送规则。
- 权限强校验需要调用组织上下文时,通过明确接口完成。
这样做牺牲了一些实时一致性和实现简单性,但换来了模型边界和故障隔离。组织结构频繁同步时,不会把消息主链路拖死。
6. 核心域、支撑域、通用域如何影响投入
DDD 的一个重要价值,是帮团队决定“哪里值得复杂,哪里必须简单”。
如果会话与消息是核心域,就值得认真设计:
- 消息 ID 与幂等。
- 消息状态机。
- 已读未读模型。
- 撤回、编辑、引用、转发规则。
- 大群扇出策略。
- 多端同步游标。
- 加密消息和普通消息的差异。
如果短信验证码是通用域,就不应该投入大量模型设计。封装一个清晰的 provider 接口,接入供应商即可。
如果审批是支撑域,但未来可能成为核心域,那早期可以先保持模块化边界,不必马上设计成独立流程引擎。等审批规则、表单编排、自动化流转真正成为产品竞争力,再提升建模投入。
战略设计的价值就在这里:它不是让所有模块都“DDD 化”,而是让团队知道哪些地方必须精细建模,哪些地方保持朴素即可。
7. 没有标准答案,但有判断依据
领域划分没有唯一标准答案。尤其在账号、组织、关系、群组这些边界上,不同团队很容易给出不同方案,而且各有道理。
比如好友关系应该放在账号域吗?
如果产品很简单,好友只是账号之间的一个双向关系,放在账号模块里可以接受。这样实现快、调用少、事务简单。
但如果好友关系逐渐演化出申请、备注、分组、黑名单、外部联系人、客户关系、关系权限、隐私设置,它就不再只是账号的附属字段,而是一个独立关系上下文。
再比如群聊应该属于消息域吗?
如果群只是消息投递目标,把群放在消息模块里似乎合理。但如果群有群主、管理员、入群审批、群公告、群禁言、群机器人、群文件、群权限,群组模型就应该独立出来,消息域只消费“可发送到某个会话”的结果。
所以判断一个边界是否合理,要看:
- 业务语言是否清晰。
- 变化是否能局部化。
- 一致性边界是否自然。
- 团队职责是否匹配。
- 部署和调用成本是否可接受。
- 未来演进时是否有可迁移路径。
DDD 的严肃性不是“切得最细”,而是“切得有理由”。
8. 战略设计的产出应该是什么
一次有价值的战略设计,至少应该产出这些东西:
- 领域全景图。
- 子域分类:核心域、支撑域、通用域。
- 限界上下文列表。
- 每个上下文的职责、不应承担的职责。
- 上下文之间的关系图。
- 关键领域事件。
- 数据所有权说明。
- 当前阶段的拆分优先级。
这些产出不一定都要画成正式 UML。白板、表格、Markdown、Mermaid 图都可以。关键是团队能围绕这些产出讨论,并且在代码和部署里逐步兑现。
下一篇我们进入战术设计,看实体、值对象、聚合根、领域服务和领域事件如何把战略边界落到代码模型中。