聊到DDD,我心里最想谈的不是实体、聚合、领域服务这些战术组件,而是一个常常被轻轻带过的词组:深层模型。项目标题是“深层模型与重构”,但市面上的资料几乎都在讲Repository怎么封装、Event怎么发、CQRS怎么拆分,很少讲清楚一个问题:重构到底是在改代码结构,还是在改我们对业务领域的理解?我自己的经验是,很多团队改了一大批代码,DDD仍然只停留在“包名好看”的层面,核心原因就是模型太浅。模型太浅,重构不过是一场大规模搬砖;模型有一定深度,重构才有机会让系统真正贴近业务本质。
这篇文章不是DDD入门手册,而是一份关于“怎么靠重构逼近深层模型”的实战复盘。适合那些已经知道实体、值对象、领域服务、聚合这些概念,但在自己的老项目上不知道怎么付诸实践的开发者。我会从一个很具体的观察出发——大多数业务系统里,模型已经变成了“数据库表结构的倒影”——然后结合我自己做费用结算模块重构的经历,拆解深层模型到底长什么样、为什么它能抵抗业务复杂度、以及如何用重构一步步把它“挖”出来。
1. 先拆掉一个误解:DDD重构到底在重构什么
很多人一提到DDD重构,第一反应就是:把原先的Service层拆开,把Repository接上,把实体类从贫血改成充血,给类名换成领域术语。这套动作看起来确实很“DDD”,但做完之后代码还是那个代码,只是穿了一层新衣服。原因很简单,因为大家做的只是代码结构重构,不是模型重构。
代码结构重构的目标是改善可读性、可维护性,行为边界保持不变。你抽方法、拆类、调整继承关系,这些都是结构层面的事,业务规则怎么算还是怎么算。但DDD里更关键的是另一类重构,我称它为模型演进。模型演进不是把现有的行为保持不变,而是发现原来的模型根本没有表达业务真正的规则,然后让新模型带着不一样的行为去替换旧行为。这是两条完全不同的路。
举一个最典型的场景。很多系统里有个“状态”字段,比如订单有未提交、已提交、审批通过、已执行、已归档这些值。写查询的人可能觉得很直观,写业务逻辑的人就开始痛苦了,因为“审批通过且执行了一部分然后又被部分退回”这种事,一个状态字段根本表达不了。于是各种判断就开始在Service层里野蛮生长:
java复制if (order.getStatus() == Status.EXECUTED
|| order.getStatus() == Status.PARTIAL_SETTLED
|| order.getStatus() == Status.ARCHIVED) {
// 可以做什么
}
这种代码多了以后,团队会干一件事:制定一个“状态只能往前进不能往后退”的规矩,放在评审文档里,或者靠代码注释提醒后来的人。但模型本身没有承载这个约束。任何一个粗心的开发都可以直接调一个setStatus()把状态改回去,系统也拦不住。等到业务方提出“我们允许运营人工把已归档的单据重新打开”,这种约束就变成了一堵必须拆掉的墙。
DDD重构要解决的就是这个问题。它不是在代码层面加固“状态不能回退”这个规则,而是先质疑:“状态不能回退”是不是我们对业务流程的正确理解?真实世界里一张单据的撤销、退回、红冲,可能根本不是同一个状态的线性流转。这时模型重构的方向就不是用状态机把“状态不能回退”封装得更稳固,而是引入“单据版本”“有效区间”“作废标志”这些更符合真实业务的深层概念,让系统能够正确表达“历史记录不可改,但新版本可以产生”。
这就是为什么我最开始总是提醒团队:一次DDD重构,首先要想清楚这次重构的产出到底是“行为不变的代码结构更好看”,还是“行为变化了但因为新模型更接近领域事实所以业务是对的”。前者不需要DDD,普通的重构手法就够用,后者才是DDD真正发挥价值的地方。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 看模型是不是浅了:我习惯先查这六个信号
不是所有系统都值得做大规模建模,但有些系统有明显的“浅模型病”。我在拿到一个旧模块开始评估时,不会急着看UML或者画架构图,我会直接按下面这几个信号去代码里找,找到两三个,基本可以判断模型是浅的。
信号一:实体类退化成了纯数据容器
类里只有一堆私有字段、getter/setter、equals/hashCode,没有任何体现业务意图的方法。这种类本质上是数据库表结构或者DTO的直接映射,有人叫它贫血模型。贫血本身不是罪,如果它只是持久化边界的载体,后面会有别的机制负责业务逻辑,那也说得过去。但如果系统的核心业务对象全是这种,业务规则就只能全部堆在Service层,Service层就会越来越臃肿。
**信号
