DDD 落地一年:我们用了什么,丢掉了什么

DDD 落地一年:我们用了什么,丢掉了什么

网上关于 DDD 的文章,大部分要么在介绍概念(什么是聚合根、限界上下文、领域事件),要么在讲成功案例(我们用了 DDD 之后系统多清晰多优雅)。

这篇不是那种文章。

我想写的是:我们团队实践 DDD 一年之后,真实的状态是什么。用了什么,没用什么,有哪些确实有帮助,有哪些折腾完之后发现是自找麻烦。


一、为什么开始用 DDD

不是主动跟风,是被问题逼的。

大概是 2021 年底,项目已经跑了三四年,代码量很大,新需求改动一个地方往往会影响到另外几个地方,改 bug 经常改出新 bug。主要的问题是:核心业务逻辑散落在各处,同一个概念在不同模块里有不同的名字和实现,模块之间的边界模糊。

那段时间我们做了一次架构复盘,讨论到”怎么给系统划清楚边界”时,有人提到了 DDD。于是开始认真研究了一下。


二、我们实际落地的内容

大概花了两个月看资料、讨论,然后开始在一个新模块里尝试落地。

落地了的:

限界上下文。这是 DDD 里落地起来最轻的一个概念,也是最有实际价值的。我们做的事情很简单:画了一张图,明确哪些业务概念属于哪个模块,跨模块的调用只能走接口,不允许直接调用对方的数据库或内部服务。

这件事做完,最直接的变化是:开会的时候大家知道在说同一件事了。以前说”订单”,有时候说的是下单流程,有时候说的是售后流单,现在有了边界,上下文清楚了,沟通效率确实提高了。

统一语言。和产品、测试对齐同一套术语,代码里的命名向业务靠拢,不再用 orderInfoorderDataorderDetail 这些模糊的名字,统一叫 Order(正式下单的订单)、OrderDraft(草稿)、AfterSalesOrder(售后单)。

应用层/领域层分层。把业务规则(”什么情况下可以取消订单”)放进领域层,把流程编排(”取消订单需要调哪些接口、发哪些事件”)放进应用层。这个分层在理解上不难,但改造成本不小。

没有完整落地的:

聚合根体系。书上说聚合根是”维护业务不变量的边界”。实际落地时,团队里对”什么是聚合根”的理解不一致,有人认为 Order 就是聚合根,有人觉得 Order + OrderItem 才算,争了好几次没有结论。后来我们的做法是:对业务规则比较复杂的核心实体(Order、User)按聚合根的思路来管理,其他的不强制。

仓储模式。理论上仓储层应该屏蔽持久化细节,领域层不感知 MyBatis 或 JPA。但项目里已经大量使用了 MyBatis,仓储接口的实现最终还是 XxxMapper,换了一层皮,本质没变。后来我们保留了接口定义,但没有要求所有模块都实现,主要给核心聚合用。


三、遇到的最大阻力

团队内部的认知分歧。DDD 的概念本身就有一定的门槛,团队里每个人看完书之后理解的侧重点不一样。有人觉得分层分得很清楚,有人觉得这不就是把代码多加了几层吗。

有一次在 review 一段代码,我觉得某个业务判断应该在 Domain 层,另一个同学觉得放 Service 里更直接,理由是不需要为了 DDD 而 DDD。

争论本身没有问题,但如果团队里没有形成基本共识,实践起来会很累。后来我们的处理方式是:不要求全员都理解所有概念,只要求关键的几个:限界上下文边界要清晰,统一语言要遵守,分层里不能乱调(Controller 不能直接调 Mapper)。

和现有代码的兼容。新模块好说,从头按 DDD 方式来。老模块改造成本很高,改动大、风险大、收益短期看不出来,很难说服自己和团队花时间去改。最终我们的策略是:老代码能不动就不动,新加功能时按新的方式来,逐步向好的方向靠拢。


四、我对 DDD 的真实评价

用了一年,我的结论是:DDD 是一套帮助你思考系统结构的工具,而不是一套必须全盘照搬的实现规范。

值得坚持的部分:

  • 限界上下文的思维方式:不管你用不用 DDD,”把系统划成有清晰边界的模块,跨边界只走接口”这件事本身就是对的,DDD 只是给了它一个名字和一套讨论框架。
  • 统一语言:这个成本最低,收益最明显,推荐所有团队做,哪怕不做 DDD。

在我们这个规模下属于过度设计的部分:

  • 严格的聚合根体系:团队 5~10 人、系统复杂度中等的情况下,完整实现聚合根 + 仓储模式,改造成本高,收益有限,还容易制造认知负担。
  • 领域事件的完整实现:在没有高并发、没有事件溯源需求的系统里,引入领域事件 + 事件总线,很容易变成”为了设计模式而设计模式”。

一句话总结:DDD 帮我们把思路理清楚了,但我们用的是其中 30% 的东西。剩下 70% 不是不好,是在当前规模下不值得付出那个代价。