在这个项目里,我对 DDD 架构的阶段理解
这篇偏工程文,主要记我在 vchoo 这类项目里,对 DDD 架构的一些阶段理解。
前面几篇写了任务调度、异常降级、日志权限这些问题,再往回看,很多复杂度都不是某个单点技术单独带出来的。更多时候,是业务对象、状态流转、外部能力、数据存储一起压上来以后,代码开始越来越难收。也是到这个阶段,我才慢慢把 DDD 当成一套能帮我整理代码和业务边界的东西来看。
1. 我当时为什么会开始认真看 DDD
刚开始做后端时,很多项目都可以先用比较直接的方式往前推:
- Controller 接请求
- Service 里写逻辑
- Repository 查库
- 返回结果
在功能比较少的时候,这种方式其实完全够用。
但 vchoo 这种链路一长,问题会很快冒出来。
因为系统里同时有这些东西:
- 创作任务
- 故事结果
- 分镜对象
- 生图任务
- 状态流转
- 重试、取消、超时、降级
- 后台配置和运营动作
这些对象往几张表里一摆,事情并不会自己变简单。它们彼此关系很强,很多逻辑靠普通增删改查也说明不白。
这个阶段如果继续把所有规则都往一个 Service 里堆,短时间看还能跑,后面会越来越难改:
- 状态判断分散
- 逻辑重复
- 某些规则写在接口层,某些写在数据层
- 一次需求变动会连着牵动很多地方
所以我后来才真正开始认真看 DDD。
对当时的我来说,它最实际的价值在这里:
能不能把业务规则和对象关系收进一个更稳定的结构里。
2. 我现在对 DDD 的第一层理解:先别把它想成理论,先把它当成“给业务收边界”的方法
一开始接触 DDD,很容易被术语压住。
比如:
- 聚合
- 实体
- 值对象
- 仓储
- 领域服务
- 应用服务
如果一开始就从定义背,挺容易空。
我后来更能接住它的方式,其实很朴素:
先看项目里哪些东西是真正的业务对象,哪些规则必须跟着这些对象走,再决定代码应该怎么摆。
这个角度会更落地。
因为 DDD 当时最能帮到我的,是逼着我把这些问题先问清楚,而不是急着多写几层文件:
- 这个对象到底是什么
- 它的状态应该由谁维护
- 哪些操作是这个对象自己该管的
- 哪些动作跨对象了,要放到更高一层处理
这些问题一旦问明白,很多代码摆放其实就不再只是风格问题,而开始和业务边界直接相关。
3. DDD 在我这里最先落地的,是分层思路
如果先不讲太细,我现在对 DDD 最先能用上的,是它把项目拆成了几层职责更清楚的东西。
1)接口层
负责:
- 接请求
- 做基础参数校验
- 调应用层
- 返回结果
这一层尽量别背业务规则。
2)应用层
负责:
- 协调整个用例
- 组织一次业务动作的执行顺序
- 调领域对象 / 领域服务 / 仓储
这一层更像“流程编排层”。
3)领域层
负责:
- 核心业务对象
- 业务规则
- 状态变化约束
- 对象之间的重要关系
这一层是 DDD 最核心的地方。
4)基础设施层
负责:
- 数据库存取
- 第三方服务调用
- 消息、缓存、文件、外部接口
这层更多是在解决“怎么接出去”。
这四层一旦分清楚,很多以前混在一起的东西就有地方放了。
4. 对我来说,DDD 更难的地方在“聚合边界到底怎么定”
分层好理解,聚合边界更难。
因为这件事说到底不是把类挪个位置那么简单,它实际上是在回答一个更核心的问题:
哪些东西应该被当成一个整体来维护一致性。
这句话一开始挺抽象,但放到 vchoo 里就会具体很多。
比如:
- 一次创作任务和它的整体状态,是不是一个整体
- 分镜是不是创作任务下面的子对象
- 生图任务要不要和创作任务绑得特别死
- 哪些状态更新必须在同一个边界里保证一致
我当时最容易走偏的地方,就是想把所有相关对象都硬塞进同一个大聚合里。
后来慢慢会觉得,这样通常不合适。
因为对象一多、状态一多、外部调用一多,一个聚合如果过大:
- 写起来很重
- 状态更新很容易互相牵扯
- Repository 查询和保存也会越来越笨重
所以我后来更能接受的理解是:
聚合边界真正要看的,是哪些规则必须在一次业务变化里一起保证。
这个标准比“它们看起来都有关联”要实用得多。
5. 结合 vchoo 这条链,我当时更容易把“创作任务”看成一个核心聚合入口
在这个项目里,如果只是从数据表角度看,故事、分镜、生图任务都有关联。
但从业务动作看,它们的变化节奏并不完全一样。
我当时比较能抓住的一点是:
- 一次创作任务有它自己的生命周期
- 分镜是这次创作下面的中间对象
- 生图任务又是后面异步执行的一组子任务
所以如果硬把所有东西都当成一个“超大对象”一次性处理,很多逻辑会很别扭。
反过来,如果完全拆散,也会失去业务上的主线。
所以我当时会更倾向于把“创作任务”理解成一个核心入口,它负责:
- 记录一次完整创作从开始到结束的主状态
- 维护它当前进行到哪一步
- 约束某些关键动作能不能发生
而分镜、生图任务这些对象,再根据它们自己的变化节奏往下拆。
这个拆法至少对我当时是有帮助的。
因为它让我不再只是按表来想系统,而开始按业务动作和一致性来想。
6. 实体和值对象这层,我现在更看重“有没有业务含义”,而不是“语法像不像类”
刚开始学 .NET 时,很容易把实体和值对象都看成普通类。
后来接触 DDD 以后,我才慢慢更在意一件事:
这个类在业务里到底有没有独立意义。
如果有自己的身份标识、状态变化、生命周期,那它更像实体。
如果只是某一组描述信息、配置值、组合值,它更像值对象。
这个区分一开始对我最大的帮助,是防止我把所有东西都写成“带几个属性的普通类”。
因为一旦什么都一样,代码虽然能写,但业务含义会越来越平。
而 DDD 想保住的,恰恰就是这层业务语义。
7. Repository 在 DDD 里对我最大的提醒,是“别让领域层自己知道数据库细节”
以前写简单项目时,很容易在 Service 里一路把查询、拼接、保存全写完。
这当然能跑。
但当业务对象越来越重、状态规则越来越多时,这样写会让领域逻辑和数据存取细节缠得很紧。
Repository 这一层对我最大的帮助,其实是把一个边界立清楚了:
- 领域层关心对象和规则
- Repository 负责把这些对象取出来、存回去
Repository 放在这里,更像是在隔开:
- 业务世界
- 数据存取世界
这个边界一清楚,后面很多代码就不至于一边处理业务规则,一边还在拼 SQL 或纠结存储细节。
8. 应用服务和领域服务,我现在先按“编排”和“规则”来分
这也是我后来比较有感觉的一块。
一开始最容易把所有逻辑都往 Service 里塞。
后来才慢慢区分:
应用服务
更适合负责:
- 一次用例怎么跑
- 调哪些对象
- 先后顺序是什么
- 最后返回什么结果
比如:
- 发起一次创作任务
- 推进到分镜拆分
- 触发生图子任务
- 汇总状态并返回给前端
这些都更像应用服务在做的事。
领域服务
更适合负责:
- 某些跨实体、跨对象的核心业务规则
- 又不太适合挂在某一个单独实体上的逻辑
这个我当时接触得不算特别深,但至少开始知道:
并不是所有逻辑都该塞在实体里,也不是所有逻辑都只能扔进“一个超大 Service”里。
这个认识对我很重要。
因为它给了我一种更细的摆放方式。
9. DDD 在这类 AI 项目里最值钱的地方,是能帮我把“状态”和“规则”慢慢收回业务对象
前面写任务调度、异常降级、日志权限的时候,其实已经反复碰到一个问题:
- 状态多
- 规则多
- 外部能力不稳定
- 一步失败会影响下一步
如果没有一个稳定的业务结构,这些规则很容易分散到:
- Controller
- Service
- 回调处理
- 数据层
- 定时任务
哪里方便就写哪里。
短期当然跑得动,后面会越来越难维护。
而 DDD 对我最有帮助的地方,就是它会一直逼着我问:
- 这个状态变化到底归谁管
- 这个规则应该挂在哪个对象上
- 这次业务动作的边界在哪
- 这个地方是在做业务判断,还是在做基础设施调用
这些问题一问出来,很多以前散着写的逻辑就开始有机会往回收。
10. 我现在对 DDD 的阶段理解,更多是“够用”,不是“教科书完整”
这一点我也想单独记一下。
因为 DDD 很容易走两个极端:
一个极端是完全不管
所有东西都按 CRUD 写,能跑就行。
另一个极端是过度设计
项目还没多复杂,先把术语、模式、层级全堆满。
我现在更能接受的,是中间这条路:
DDD 最适合拿来处理项目里已经出现的业务复杂度,没必要为了“像 DDD”把结构先堆重。
这对我来说很重要。
因为它让我不会一边学架构,一边脱离手上的真实业务。
我现在会更看重:
- 这套拆法对当前项目有没有帮助
- 状态和规则是不是更清楚了
- 后面改需求时是不是更好收口了
这些比“术语是不是用得很标准”更有意义。
11. 这篇先记一个当前够用的版本
先给自己留一个阶段性结论:
- DDD 对我最直接的价值,是帮我给业务对象、状态和规则收边界
- 分层不算最难,真正绕人的还是聚合边界怎么定
- 应用服务负责用例编排,领域层负责核心业务规则,基础设施层负责把能力接出去
- Repository 的意义在于隔开业务对象和数据存取细节
- DDD 在这类 AI 业务项目里最值钱的地方,是让状态和规则不至于散得到处都是
这篇先写到这里。
