DDD 不是画一堆漂亮的类图
一提到领域驱动设计,很多人脑中浮现的是几张画满实体、箭头、继承关系的 UML 图。但 DDD 真正解决的问题不在画图,而在于:当业务变得复杂,代码和业务逐渐对不上,该怎么让它们重新咬合。
它的核心不是技术手法,而是一套围绕业务建模的思考方式。业务复杂的系统,难点从来不是编程语言,而是「这个系统到底在帮业务做什么」。DDD 就是逼你把这件事想清楚,再落到代码结构上。
模型不是设计的产物,而是团队对业务达成共识之后的记录。
- 统一语言业务与开发共用一套术语,不各说各话。
- 限界上下文不同业务规则住进不同的边界里。
- 聚合一组需要一起保持一致的对象,用一个根管住。
- 事件风暴把业务流程可视化,靠讨论发现边界。
- 渐进落地从一个最复杂的上下文做起,别一次铺开。
统一语言:先让业务和开发说同一种话
大多数系统的混乱,源头是同一个词在不同人嘴里意思不同。业务说的「订单」,在开发那里可能指数据库里一张表,在客服那里又指一次售后申请。词不统一,讨论就一直在空转。
统一语言的要求很朴素:在一个业务范围里,同一个概念只能有一个名字,这个名字同时出现在对话、文档和代码里。代码里的类名、变量名,就应该直接用业务用的词。
# 坏味道:技术命名掩盖了业务含义
class OrderProcessor: # 「处理」到底在处理什么?
def update(self, data):
…
# 统一语言后:名字就是业务动作
class 订单履约: # 业务说的就是「履约」
def 确认发货(self):
…
# 这段代码读起来,像在复述业务规则本身。
当代码读起来像业务描述,业务方看代码也能对上号,评审和沟通的成本都会下降。
限界上下文:给系统划清边界
同一个词在不同业务场景里本来就可以有不同含义。「商品」在售卖场景是可下单的货,在库存场景是可盘点的货,在财务场景是一笔账。硬塞进一个「商品类」,必然处处别扭。
限界上下文就是承认这件事:把系统按业务规则分成若干边界,每个边界里用自己那套统一语言。边界之间通过明确的接口协作,而不是共享同一堆类。
| 边界 | 关心的问题 | 「商品」的含义 |
|---|---|---|
| 售卖 | 怎么下单、怎么促销 | 可被加入购物车的货 |
| 库存 | 还剩多少、放哪 | 可被盘点的存货 |
| 财务 | 记多少账、怎么开票 | 一笔可计价的资产 |
边界一旦清晰,团队就可以按边界分工,一个上下文由一个小团队独立负责,不再互相踩对方的领域。
实体、值对象与聚合
进入一个上下文内部,建模时有几个常用的积木。实体是有身份、会变化的东西,比如一笔订单;值对象是只由属性定义、不可变的描述,比如一个金额或地址。
聚合是最关键的概念:一组对象必须放在一起才保持业务上的一致,就用一个聚合根把它们管住。外部只能通过聚合根来操作,不能绕过它直接改内部对象。
# 聚合示例:订单是聚合根,订单项是它的内部部分
class 订单:
def __init__(self):
self.项 = [] # 订单项必须通过订单来增减
def 添加商品(self, 商品, 数量):
# 由订单负责:检查库存、算总价、记录快照
…
def 确认(self):
# 整条订单要满足业务规则才能确认
if not self.项:
raise 订单为空()
…
聚合的意义是把「哪里必须一致」这件事显式化,避免对象之间互相乱改、规则散落在各处。
事件风暴:把业务流程摊在桌上
怎么找到限界上下文?靠一个人在会议室里拍脑袋分,往往分错。事件风暴是一个接地气的办法:把业务人员、产品、开发拉到一起,用便利贴把业务里发生的事一条条贴出来。
- 先写「领域事件」:已经发生的业务事实,比如「订单已下单」「货已发出」。
- 再围绕事件找命令、找参与者、找聚合,观察哪些事件天然聚成一团。
- 团与团之间的缝隙,往往就是限界上下文的边界。
| 步骤 | 做什么 | 产出 |
|---|---|---|
| 列事件 | 把业务里发生的事写在便利贴上 | 一墙的领域事件 |
| 分组 | 把相关事件圈在一起 | 几个候选业务模块 |
| 定边界 | 讨论模块之间怎么协作 | 初版限界上下文图 |
它的价值不在那张图,而在讨论本身——业务和开发在贴便利贴的过程中,把以前没说清的分歧全暴露出来。
什么时候该上 DDD,什么时候不必
DDD 是为复杂业务系统准备的,不是银弹。一个简单的官网、一个内部小工具,硬套 DDD 只会徒增复杂度。
# 适合引入 DDD 的信号:
- 业务规则多、且经常变
- 同一个概念在不同场景含义不同
- 团队经常因为理解不一致返工
- 核心业务值得长期维护演进
# 不必硬套 DDD 的信号:
- 逻辑主要是增删改查和展示
- 业务简单、几乎不变化
- 一次性、短期使用的工具
# 这些地方用直接清晰的结构就够了。
DDD 的成本在于讨论和建模,只有当业务复杂到值得这份投入时才划算。
DDD 落地从一个上下文开始
最容易失败的 DDD 落地方式,是一上来就想给整个系统重新建模。正确的姿势是挑出最复杂、最痛的那一个限界上下文,先在它里面跑通统一语言和聚合。
- 选一个业务核心、改动频繁的上下文,做一次事件风暴,定下边界与聚合。
- 让这个上下文的代码结构贴合模型,团队先尝到「代码即业务」的甜头。
- 跑顺之后,再逐步把相邻上下文按同样方式整理,不追求一步到位。
领域驱动设计最终给你的,不是一套完美的图,而是一种习惯:每次写代码前,先问自己这段代码在业务里到底在做什么。把这件事坚持下去,系统就不会越长越散。