《领域驱动设计》读书笔记:领域事件与限界上下文
上一篇读书笔记聊了 DDD 的聚合根,这篇接着聊两个更高层次的概念:限界上下文(Bounded Context) 和 领域事件(Domain Event)。这两个概念是解决"大型系统如何演进"的核心工具。
限界上下文:一个被反复误解的概念
Evans 在书里把限界上下文定义为:“一个模型的应用边界。”
听起来很抽象,但实操上很容易理解:同一个词在不同上下文里意思不同。
举例:“用户"这个词在不同子系统的含义:
- 认证上下文:“用户"是一个能登录的账号(关注用户名、密码、登录状态)
- 订单上下文:“用户"是下单的人(关注收货地址、联系方式)
- 营销上下文:“用户"是一个可以被推荐系统识别的对象(关注偏好、行为轨迹)
- 财务上下文:“用户"是付款人(关注支付方式、账单地址)
如果硬要把"用户"在所有上下文中统一成一个模型,你最终会得到一个"上帝类”——既臃肿又难维护。
正确的做法是承认:每个上下文有自己的"用户"模型,它们是不同的领域对象,只是共享一个 ID。
限界上下文与微服务
微服务流行的当下,“限界上下文"几乎成了微服务拆分的事实标准。
每个微服务对应一个限界上下文:
- 拥有自己的数据库
- 拥有自己的模型
- 通过明确的接口(API、消息)和其他上下文通信
但这里有个常见误区:不是每个限界上下文都要做成微服务。
小团队、单体应用阶段,可能一个单体包含多个限界上下文(用模块/包区分)。只有当团队规模扩大、独立部署需求出现时,才把限界上下文拆成微服务。
上下文映射:跨边界如何协作
不同限界上下文之间如何通信?Evans 提出了"上下文映射”(Context Mapping)的概念,给出了几种典型模式:
1. 共享内核(Shared Kernel)
两个上下文共享一部分模型。比如订单和库存都共享"商品 SKU"这个概念。
风险:共享部分的变化会同时影响两边。需要双方团队协调。
2. 客户-供应商(Customer-Supplier)
上游"供应商"提供模型,下游"客户"使用。比如支付上下文给订单上下文提供支付能力。
风险:上游变化会影响下游。下游要有 SLA 约束。
3. 防腐层(Anti-Corruption Layer, ACL)
下游用一个翻译层把上游的模型翻译成自己的模型。这样上游变化不会直接污染下游。
这是最实用的模式之一。当你的系统要集成一个老旧的外部系统时,ACL 是必选项。
4. 开放主机服务(Open Host Service, OHS)
上游定义一套稳定的协议(API、消息格式),多个下游都可以接入。
这是微服务最常用的模式。
领域事件:跨上下文的协作方式
如果说限界上下文定义了"边界”,领域事件就是"边界之间通信的语言”。
领域事件的核心思想:用过去时态描述已经发生的事。
OrderPlaced(订单已下单)PaymentReceived(支付已收到)InventoryReserved(库存已预留)ShipmentDispatched(包裹已发出)
每个事件都是不可变的、过去式的、明确的。
事件 vs 命令
初学者容易混淆领域事件和命令。两者的区别:
命令(Command):希望某件事发生(动词,祈使语气)
PlaceOrder(下单)ChargePayment(扣款)
事件(Event):某件事已经发生(过去时态)
OrderPlaced(订单已下)PaymentCharged(支付已扣)
从消息系统的角度,命令通常是"点对点"的(一个发送者、一个接收者),事件通常是"发布订阅"的(一个发布者、多个订阅者)。
事件风暴:协作设计方法
Alberto Brandolini 在 2012 年提出了"事件风暴”(Event Storming)工作坊方法,用于协作发现领域事件。
基本流程:
- 把所有业务相关人员聚在一个房间里
- 在大白板上用橙色便签写"领域事件”(按时间顺序从左到右)
- 用蓝色便签写"触发事件的命令"
- 用黄色便签写"产生命令的角色"
- 用绿色便签写"读模型(用于查询的视图)"
- 用粉色便签标注"外部系统"
这个工作坊能在 1-2 天内让团队对齐业务流程,比写几十页需求文档有效得多。
事件溯源:极端的设计
领域事件自然引出了一个更激进的设计——事件溯源(Event Sourcing)。
传统设计:保存对象的当前状态。事件溯源:保存所有事件,通过回放事件得到当前状态。
graph LR
A[账户注册事件] --> B[账户首次充值事件]
B --> C[账户消费事件]
C --> D[账户退款事件]
D --> E[当前状态: 余额 100]事件溯源的优势:
- 完整的审计日志:所有变更都记录在案
- 可回放:可以重新计算任意时间点的状态
- 易于实现时间旅行:debug 时可以回到任意时刻
事件溯源的代价:
- 复杂度高:所有操作都要先想"它会产生什么事件"
- 查询复杂:要查询当前状态必须回放事件(用 snapshot 优化)
- 学习曲线陡:团队需要熟悉新思维
事件溯源适合少数高价值场景(金融、审计、协作工具),不适合大多数业务系统。
给我最大的启发
DDD 这一章让我意识到:
- 边界思维:好的架构不是"统一一切",而是"清晰划分边界"
- 事件即事实:领域事件是业务事实的不可变记录,比"当前状态"更接近真相
- 防腐是关键:跨系统集成的最大风险是"被上游污染",防腐层是必要的护城河
- 协作先于建模:DDD 不是个人英雄主义,而是团队共同探索业务的过程
推荐阅读
- Eric Evans《领域驱动设计》第 14 章(领域事件)、第 2 章(限界上下文)
- Alberto Brandolini《Event Storming》—— 协作工作坊方法
- Vaughn Vernon《实现领域驱动设计》—— 更现代的实践
DDD 不是一套死的规则,而是一种思考方式。它教我们如何看待业务、如何划分系统、如何让代码反映现实——这些能力在任何复杂项目中都受用。
补充:微信读书里我划过的句子
微信读书里我读的是张逸的《解构领域驱动设计》,里面有几段对限界上下文的精彩论述,比 Eric Evans 原书讲得更现代、更落地:
限界上下文是领域驱动设计战略层面最重要、最基本的架构设计单元。
先从领域维度进行纵向切分,再从技术维度对限界上下文进行横向切分,因此限界上下文是一个对外暴露业务能力的架构整体。
张逸把"限界上下文"提升到了战略级别——它不只是代码组织单元,而是业务能力单元。这种视角让我重新思考:
- 限界上下文 ≠ 微服务:一个限界上下文可以是微服务,也可以是单体里的一个模块
- 限界上下文 ≠ 数据库 schema:它是业务能力的边界,不是数据的边界
- 限界上下文 ≠ 代码包:它是团队协作的边界,不只是代码组织
当然,考虑到前后端分离的架构以及用户体验的特殊性,通常,限界上下文并不包含对展现层的纵向切分。
这一句纠正了我之前的误解——我以为限界上下文应该包揽所有层(包括 UI),但张逸指出:展现层通常独立于限界上下文。因为展现层是用户体验的载体,按用户旅程切分,不是按业务能力切分。
限界上下文更是被拔高到战略设计的核心地位,也成了连接问题空间与解空间的重要桥梁。
这一句是我重新审视限界上下文的钥匙——它是连接问题域和解域的桥梁。问题域是业务需求,解域是技术实现。限界上下文翻译两边,让业务语言变成代码语言。
最后一句让我划线:
Fowler 建议应该单体架构优先,通过该架构风格逐步探索系统的复杂度,确定限界上下文构成组件的边界,待系统复杂度增加,证明了微服务的必要性时,再考虑将这些限界上下文设计为独立的微服务。
这是非常务实的建议。微服务是结果,不是起点。先单体、后微服务——这是 Martin Fowler 的忠告,也是张逸反复强调的观点。
我曾经犯过的错误是"先拆微服务",结果为了"微服务"而牺牲了系统复杂度演化的空间。这本书让我重新回到务实的起点。