只要搞过后端架构的人,多少都经历过这种尴尬:DDD(领域驱动设计)的书翻了好几遍,事件风暴工作坊也开过,架构评审会上大家点头如捣蒜。结果代码一开工,写着写着又变回了三层架构,领域层被Service掏空成贫血模型,聚合根只剩一堆getter/setter。DDD不是不好,是落地的成本实在太高了,高到大多数团队撑不过前三个迭代。
所以我搞了个小项目,叫cleanddd-skills。思路很直接:既然人肉遵守规范的成本这么高,那就把这些规范翻译成AI能读懂、能执行的技能包,让AI编程工具在产出代码的时候,天然带上DDD的约束。这篇文章就把这个项目的设计思路、技能包的长什么样、以及我实际操作中的一套完整流程,全部摊开来讲。适合正在做DDD转型、或者被AI生成的“四不像”代码搞到头大的后端团队参考。
1. 先聊清楚:DDD难落地,到底难在哪
1.1 难点一:建模过程太依赖“老司机”
DDD的建模方法论本身不复杂,核心就是找限界上下文、画上下文映射、做事件风暴、识别聚合。听着挺像那么回事,做起来完全是另一码事。
最扎心的一个场景是:事件风暴工作坊上,业务专家、产品经理、开发围坐一圈,黄色便签贴了一整面墙。开发问“订单取消这个事件,取消的原因算值对象还是实体”,业务方一脸茫然;业务方说“我们就是支持客户提交申请之后可以撤回”,开发立刻开始纠结“撤回”和“取消”到底是不是同一个领域事件。这种语言层面的错位,如果没有一个经验丰富的DDDer在中间做翻译和引导,会议大概率会变成需求澄清会,最后产出物跟普通PRD没有任何区别。
而且即便建模当时是清楚的,从“墙上的便签”到“代码里的聚合”,中间还有一条巨大的鸿沟。建模画出来的聚合边界,落到代码里往往就被一个UserService、OrderService给揉碎了。这里面的关键问题不是大家不懂DDD,而是缺少一个能把“建模语言”持续翻译成“代码约束”的工具。
1.2 难点二:代码层面缺少持续约束,写着写着就“腐化”
DDD进入编码阶段后,真正的考验才刚开始。聚合根内部状态应该由行为方法保护,结果为了前端方便,给实体加了public setter;领域服务应该表达业务编排,结果把所有逻辑塞进应用服务,应用服务反过来依赖基础设施,形成依赖倒置。这些坏味道不是一次写崩的,而是在几十个迭代里一点点“腐化”出来的。
你当然可以说“我们有Code Review”。但现实是,大部分团队的Code Review都在看“这个功能对不对”,很少有人会逐行检查“这个聚合是否暴露了不该暴露的setter”、“这个仓库接口是否被应用层直接实现给绕过去了”。DDD约束本质上是一种全局性、结构性约束,靠人眼去盯着,覆盖率一定是不足的。
1.3 为什么AI能在这个环节“打辅助”
我试过让AI直接从一个需求描述生成完整业务系统,效果只能说是“看起来合理,实际不能细看”。但AI有个长处非常难得:你给它一套明确的规则,它能像强迫症一样严格执行,而且从不觉得烦。
DDD落地的痛点,恰好就是“需要有人不厌其烦地执行规则”。AI当然不能替代领域专家,但它可以成为那个“随时在线、永不疲倦的规范执行器”。你在需求描述后面追加一段“按DDD规则建模”的指令,AI就会老老实实地按照聚合、值对象、领域事件的框架去思考和产出;你在项目里放好一条“禁止在实体上暴露public setter”的规则,AI生成的代码就不会违反这条约束。
这就是cleanddd-skills的核心逻辑:把DDD落地的各种规范、检查清单、坏味道案例,结构化成一个AI能直接调用的“技能包”。AI不用再猜“DDD到底要怎么落地”,它只需要按照技能包里写的步骤和规则去工作。
1.4 这个项目跟我见过的其他方案有什么不同
市面上关于“AI+DDE”的方案,大多停留在“写提示词”的层面。比如有人会分享一个很长的prompt,让AI扮演DDD专家。但单一的提示词有两个问题:一是太长,超过上下文窗口之后效果急剧下降;二是不可维护,改一条规则就要把整个prompt翻出来重新改一遍。
cleanddd-skills的做法是模块化。把“建模引导”、“代码生成”、“代码审查”、“上下文映射”拆成独立的技能文件,每个技能文件负责一个环节,AI按需加载。你可以单独使用“审查技能”去扫描旧代码,也可以把“建模技能”和“编码技能”串联起来,形成一条完整的DDD流水线。这种拆分方式,不管是对团队协作还是后续更新,都要比“一个大而全的提示词”靠谱得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. cleanddd-skills的整体设计与核心模块
2.1 Skills机制到底是什么:给AI装一套“岗位SOP”
如果你已经用过Claude Code、Cursor这类AI编程工具,应该对Skills概念不陌生。简单来说,一个Skill就是一组包含“指令说明+参考规则+示例模板”的文件目录,AI在执行相关任务时会自动读取这个目录下的规则文件,从而获得该领域的“专业知识”。
打个比喻:你新招了一个开发,他不会自动知道你们团队的规范,你得给他发一份岗位SOP。Skills就是这份SOP,而且不是写给人看的,是给AI读的。我做的cleanddd-skills,就是把DDD实践者的经验,整理成四个Skill文件包,每个包都有明确的触发场景、执行步骤、检查清单和反例库。
项目结构上是这样的:
text复制cleanddd-skills/
├── ddd-modeling/
│ ├── SKILL.md
│ ├── checklists/
│ │ ├── aggregate-design-checklist.md
│ │ └── event-storming-questions.md
│ └── examples/
│ ├── order-modeling.md
│ └── shipping-modeling.md
├── ddd-coding/
│ ├── SKILL.md
│ ├── templates/
│ │ ├── aggregate-root.java
│ │ ├── value-object.java
│ │ └── repository-interface.java
│ └── rules/
│ └── dependency-rules.md
├── ddd-review/
│ ├── SKILL.md
│ └── anti-patterns/
│ ├── anemic-model.md
│ ├── leaking-repository.md
│ └── setter-exposure.md
└── ddd-context-map/
├── SKILL.md
└── templates/
└── context-map.mermaid.md
注意,虽然我不在正文用mermaid,但技能包里允许AI生成mermaid来画上下文映射图,这是内部模板,不影响文章阅读。
每个SKILL.md文件里,我都会用固定的结构去描述:这个技能在什么场景下激活、需要哪些输入、按照什么步骤执行、输出要符合什么标准、有哪些绝对不能出现的反模式。这种结构化描述,让AI不会跑偏。
2.2 建模技能(ddd-modeling):把模糊需求翻译成领域模型
建模技能的定位是“从一段业务描述出发,产出符合DDD战术建模的领域模型草案”。它的核心价值,是强制AI在写代码之前先过一遍模型设计。
这个技能的SKILL.md里,我写了一段类似这样的执行指令:
text复制当用户要求分析一段业务需求时,遵循以下步骤:
1. 首先识别业务中的关键动词,产出候选领域事件列表。
2. 识别参与这些事件的角色与业务对象,区分实体、值对象、聚合根。
3. 对每个候选聚合,检查聚合内的每个对象是否真的共享同一个事务边界。
4. 走一遍聚合设计检查清单,确认聚合设计没有明显问题。
5. 输出:领域事件列表、聚合清单、每个聚合的核心行为(方法签名)、聚合间的关联关系(按ID引用)。
6. 未经用户确认,不要直接进入代码生成。
这里有一个关键设计:强制暂停。很多AI编程失败案例,都是因为AI太急着写代码,需求还没搞清楚就开干,结果生成的模型到处都是隐性假设。建模技能里强调“未经确认不进入代码”,就是逼着AI把模型先摊在桌面上,让人类架构师去审。
配套的aggregate-design-checklist.md,是我较得意的一部分。里面列出了聚合设计需要逐条确认的问题:
- 聚合内所有对象是否只有通过聚合根才能被外部访问?
- 聚合根是否承担了维护内部不变量的职责?
- 聚合与聚合之间是否只通过全局唯一标识(ID)互相引用?
- 是否明确了一致性边界,而不是让所有业务规则共享一把大锁?
- 是否存在“为了放一个业务操作而强行把一个对象拉进聚合”的情况?
这些清单看起来简单,但每一条都在实际项目中踩过坑。让AI带着这些问题去审视自己的设计,能有效地减少“直觉建模”带来的灾难。
2.3 编码技能(ddd-coding):让生成的代码天然符合战术设计
建模技能解决“模型长什么样”的问题,编码技能解决“代码怎么写才能不腐化”的问题。
这个技能的指令,核心是把DDD在代码层的约束写死,让AI在生成代码时无脑遵循。我挑几条直接影响代码风格的规则说一下:
- 分层的依赖方向:领域层不依赖基础设施层和应用层;应用层依赖领域层,但不依赖基础设施层;基础设施层实现领域层定义的接口。
- 聚合根是本层的心脏:聚合内的状态变更只能通过聚合根的方法触发,禁止暴露setter或返回内部可变集合的引用。
- 值对象是不可变的:所有属性final,通过构造器全量赋值,行为方法返回新对象而不是原地修改。
- 仓储接口定义在领域层:实现放在基础设施层,应用层只面向接口编程。
- 模型对象不与持久化注解耦死:尽量避免在领域实体上直接堆一大堆Hibernate注解,把数据映射隔离到专门用于持久化的模型上,除非你的上下文足够简单。
这最后一条,在实践中最容易引发争论,因为Spring Data JPA这套东西太深入人心了。我的经验是,把“不直接复用JPA注解”作为默认规则,但对于内部管理类上下文(比如用户管理、操作日志)可以允许例外,让团队通过排除清单来控制。
为了让AI生成代码时有一个参照系,我还准备了模板文件。拿实体模板举例,模板里展示了:什么是聚合根应该有的样子、内部集合怎么封装、业务方法怎么书写。AI在生成代码时会参考这个模板,生成的结果就不会偏离DDD基本盘。
2.4 审查技能(ddd-review):给现有代码库做“DDD健康体检”
如果说建模技能和编码技能是“防患于未然”,审查技能就是“亡羊补牢”。
这个技能专门用来扫描已有代码,识别DDD反模式。我把过去几年见到的坏味道整理成了anti-patterns目录,每个反模式一个文件,包含特征描述、危害、检测方法、重构建议。举两个最常见的例子:
贫血模型(Anemic Model):特征是实体里除了getter/setter,几乎找不到业务行为;业务逻辑全部堆在Service里。出现这个模式,说明系统的领域层只是一个数据容器,通过对象承载数据的意义已经失去。重构建议是先从频繁变化的Service方法入手,识别它到底在维护哪个对象的业务规则,把方法连同状态迁移到对应的领域对象上。
仓储泄漏(Leaking Repository):特征是应用服务直接注入了具体仓储实现类,或者仓储接口里定义了查询特定业务视图的方法。这种情况会让应用层变成“数据库操作编排层”,领域服务就失去了存在的意义。正常做法是仓储接口保持薄,只提供聚合生命周期管理(查找、保存、删除),复杂的查询用CQRS思路单独处理。
审查技能在我的使用流程中,不仅用来查“存量代码”,更重要的是查AI自己刚生成的代码。把AI新写的代码喂给它,让它对照审查,能发现我自己都不一定注意到的细节问题。
2.5 上下文映射技能(ddd-context-map):画出系统间的“国境线”
最后一个技能块是上下文映射。它不负责具体的业务代码,而是负责在多个微服务、多个限界上下文之间画出边界关系。这在拆服务和做集成设计时非常关键。
用这个技能的时候,我会让AI阅读多个服务模块的业务描述或API定义,然后画出它们之间的上下文映射关系。它会区分合作关系(Partnership)、防腐层(Anti-Corruption Layer)、共享内核(Shared Kernel)等模式,并提示在哪些接口边界上需要引入防腐层来保护领域模型的纯洁性。
举个实际例子:一个订单服务和一个库存服务,订单服务需要实时查询库存数量。如果没有明确的上下文映射,订单服务很可能直接远程调用库存服务的一个内部接口,两个服务便被强耦合起来。这个时候上下文映射技能会建议:在订单服务这一侧建立一套本地接口,比如SkuStockQuery,用防腐层适配远程调用。这样,即使库存服务接口发生变化,订单服务领域层也不用跟着动。
3. 实操:在AI编程工具里把DDD跑起来(以订单模块为例)
3.1 环境准备:工具选择与技能加载
我日常工作主要用Claude Code和Cursor交替着写。这套技能包我是按Claude Code的Skills目录结构来组织的,但同样的规则稍加调整也适用于其他支持自定义指令的AI编程工具。
加载动作很简单:把cleanddd-skills整目录放到项目根目录的.claude/skills/下,AI就能在对话中自动识别并调用。如果你用的是Cursor,可以改成.cursor/rules下的Markdown规则文件,效果差不多。
注意:技能文件里我严格禁止出现“直接照搬模板、不做业务判断就生成代码”的字眼,这是为了不让AI只当一个代码打字机,强迫它结合业务输入来做推断。
3.2 第一步:让AI做领域建模
实操的第一步,我通常会把用户故事或需求描述,原封不动丢给AI,同时追加一条指令:
text复制请使用 ddd-modeling 技能分析以下需求,先输出领域事件列表和候选聚合,不要写代码。请逐条走一遍聚合设计检查清单,并指出你做了哪些假设。
需求如下:
[粘贴需求描述]
以订单模块为例,我输入一段没有经过加工的原始需求:
普通用户可以把商品加入购物车,下单时可以选择地址和优惠券,订单提交后,系统要校验库存、锁定优惠券、扣减库存并创建订单。15分钟内不支付,订单自动取消并释放库存。支付成功后,订单进入待发货状态。
AI按技能执行后的输出大致是这样的:
text复制候选领域事件:商品已加入购物车、订单已提交、库存已锁定、优惠券已锁定、库存已扣减、订单已取消、库存已释放、订单已支付
候选聚合:
- 购物车(Cart)
- 订单(Order)
- 库存(Stock)
- 优惠券(Coupon)
关键假设:支付和订单取消,是同一个聚合内的状态流转还是跨聚合事件?我这里默认支付结果由外部支付网关回调触发,属于订单内部行为;库存扣减由订单生命周期事件触发,跨聚合通信采用领域事件。
这一步质量高不高,全看AI是否走完了检查清单。有时AI会跳步,直接给出“好的,您的领域模型设计如下”,我会追问一句“请把聚合设计检查清单的逐条结果列出来”,它就会老实补充。用这个办法能逼出不少隐藏假设。
3.3 第二步:让AI生成遵循DDD的代码骨架
模型确认完,接下来就是编码技能登场。我会告诉AI:
text复制请使用 ddd-coding 技能,按以下聚合设计生成订单上下文的核心代码骨架。
要求:
- Order 是聚合根,OrderItem 是值对象,不支持独立仓储。
- 创建订单的流程走应用服务 OrderApplicationService,所有仓储依赖走接口。
- 聚合根状态变更通过方法触发,不暴露 setter。
AI生成的代码会大致长这样(节选):
java复制// 聚合根
public class Order {
private final OrderId id;
private final List<OrderItem> items;
private OrderStatus status;
private Address address;
private CouponId couponId;
private Order(OrderId id, List<OrderItem> items, Address address, CouponId couponId) {
this.id = id;
this.items = Collections.unmodifiableList(items);
this.address = address;
this.couponId = couponId;
this.status = OrderStatus.PENDING_PAYMENT;
}
public static Order create(List<OrderItem> items, Address address, CouponId couponId) {
if (items == null || items.isEmpty()) {
throw new InvalidOrderException("订单必须包含至少一个订单项");
}
return new Order(OrderId.generate(), items, address, couponId);
}
public void markPaid() {
if (!OrderStatus.PENDING_PAYMENT.equals(this.status)) {
throw new IllegalStateException("只有待支付状态的订单可以支付");
}
this.status = OrderStatus.PAID;
}
public List<OrderItem> items() {
return items;
}
}
这里值得多说一句:封装是不可讨论的底线。你看代码里这个items()方法,它返回的是List<OrderItem>,这其实藏着一个坑。如果OrderItem是不变对象,那返回不可变集合问题不大;如果OrderItem是可变实体,就应该用Collections.unmodifiableList包裹,或者返回有序的只读视图。编码技能里我补充了一条:聚合根内部集合如果要暴露,必须返回只读副本,防止外部直接修改聚合内部状态。
3.4 第三步:让AI做DDD代码审查
代码生成了,不代表大功告成。我接下来会启用审查技能,把AI生成的代码和手写的存量代码全部丢给它复查。
text复制请使用 ddd-review 技能,扫描 src/main/java/order 下所有 Java 文件,按反模式库逐项检查,输出问题文件和违规类型,并给出重构建议。
实际跑一次,AI找出的典型问题包括:
| 问题表现 | 违规类型 | 严重程度 |
|---|---|---|
OrderService 中直接调用 OrderRepositoryImpl |
仓储泄漏 | 高 |
OrderItem 有 public setter |
setter暴露 | 中 |
应用层直接访问 .getItems() 后循环修改 |
聚合内部状态外泄 | 高 |
Stock 聚合里放了商品价格字段 |
聚合边界错误 | 中 |
这些问题是真实存在的。尤其是“应用层循环修改items”这种场景,几乎是所有DDD项目都会犯的通病——为了给前端返回金额小计,直接在应用层遍历聚合内集合做计算。实际上这个计算应该定义在Order聚合本身的行为方法里,比如totalAmount()。
AI给出审查结果后,我会让它按建议直接出重构补丁,然后我再把补丁Review一遍。这个工作流比人工Review高效得多,它把“检查—分析—修复”整个闭环压缩到几分钟内。
3.5 一个完整的坏味道修复案例
用订单模块再说一个实际案例。初始代码里有一个典型的贫血模型,我被AI审查之后,它给出的重构路径非常清晰。
原始代码风格是这样的:
java复制public class Order {
private Long id;
private String status;
private BigDecimal totalAmount;
// 大量getter/setter省略
}
@Service
public class OrderService {
public void cancel(Long orderId) {
Order order = orderRepository.findById(orderId);
if (!"PENDING_PAYMENT".equals(order.getStatus())) {
throw new BusinessException("当前状态不能取消");
}
order.setStatus("CANCELED");
order.setTotalAmount(BigDecimal.ZERO);
orderRepository.save(order);
}
}
问题一眼就能看出来:订单取消的业务规则写在Service里,状态流转、金额清零这种业务动作全摊在Service做,Order对象完全是个数据壳子。
重构后的代码,把业务逻辑下沉到聚合根:
java复制public class Order {
private Long id;
private OrderStatus status;
private Money totalAmount;
public void cancel() {
if (!OrderStatus.PENDING_PAYMENT.sameAs(this.status)) {
throw new OrderStateException("当前订单状态不能取消");
}
this.status = OrderStatus.CANCELED;
this.totalAmount = Money.zero();
}
}
@Service
public class OrderService {
public void cancel(Long orderId) {
Order order = orderRepository.find(orderId);
order.cancel();
orderRepository.save(order);
}
}
这段重构我特别推荐团队里的新人看,它直观地展示了DDD“行为收拢”的核心思想。注意重构后,OrderService里没有一行业务规则,只有“取出来、调方法、存回去”三个动作。
4. 常见问题与避坑实录
这套技能包用了一段时间,自己也踩了不少坑。几个典型问题专门列出来,免得后面的人重复踩。
4.1 AI把模型做成了“过度设计”
AI一开始很容易“用力过猛”。你让它识别聚合,它恨不得把每个对象都变成聚合根,连用户地址都单独搞一个聚合。结果就是代码变得很碎,业务方法散落在几十个对象里。
我的对策是在建模技能的检查清单里加了一条硬规则:“如果没有强一致性的需求,就不要把两个概念硬塞进同一个聚合;如果不存在独立的不变式和行为,就不要单独建聚合根”,并且给了AI一个判断标准——如果你说不清楚这个聚合根要维护的核心不变量是什么,那它大概率不应该成为聚合根。
4.2 技能规则与框架规范的冲突
这个问题在引入Spring Data JPA时遇到过一次。DDD规范通常不允许实体暴露无参构造,但JPA要求实体必须有默认构造器。两者一冲突,AI就不知道听谁的了。
我在项目的rules/dependency-rules.md里加了一个例外条款:持久化框架需要的默认构造器可以存在,但必须是protected,并且不作为公共API暴露。这样既满足了框架要求,又不破坏聚合封装。这个妥协方案,在DDD实践圈里也算是一种比较常见的折中姿势。
4.3 AI的“一本正经胡说八道”问题
AI不是领域专家,它有时会脑补出业务上并不存在的领域事件。比如它可能在建模时偷偷加上一个“订单审核通过”的事件,但需求里根本没有审核环节。
为了解决这个,我在建模技能里强制要求AI在输出模型前,必须先列出“关键假设清单”,并且把每个领域事件跟原始需求里的某句话做对应映射。如果事件没有对应的原始需求支撑,就标记为“推测”,必须在人工确认后才能保留。这个机制能挡住大部分幻觉。
4.4 规则过期了怎么办
业务在发展,规范也在演进。技能包跟代码一样会面临“维护欠债”。我的经验是:每隔一到两个迭代,用审查技能对项目做一次全面体检,把新发现的反模式补充进anti-patterns目录。同时,把每次评审中“规则是否还合理”这个议题固定到团队复盘里,一旦业务对它的妥协成为共识,就立刻更新技能文件。
4.5 问题排查速查表
| 现象 | 可能原因 | 解决建议 |
|---|---|---|
| AI生成的模型边界很乱 | 未正确加载建模技能的检查清单 | 确认技能目录是否被工具加载,检查SKILL.md指令是否生效 |
| AI生成代码仍带setter | 编码技能规则未覆盖具体语言模板 | 在templates目录补充目标语言的实体模板作为参考 |
| 审查技能漏检某个坏味道 | 反模式库没有积累该模式 | 把新坏味道补录进anti-patterns目录,形成案例 |
| AI回答时完全无视技能包 | 多个技能指令冲突,或技能触发条件未被描述清楚 | 检查SKILL.md中的activation condition,明确触发场景 |
| 上下文映射技能生成的关系图太简单 | 业务描述输入不完整 | 提供更完整的服务边界描述或API清单再触发分析 |
我个人在实际操作中最深的体会是,这套玩法真正的价值不在于“AI替代了DDD专家”,而在于它把DDD的落地约束从“人脑记忆”变成了“项目基础设施”。一个刚入职两周的同事,只要让AI按技能包产出代码,产出的质量就能达到团队老手八成功力,这是传统“师傅带徒弟”模式很难做到的。
如果你也想往这个方向折腾,一个小建议:不要一上来就想把整个架构规范全部写成技能包,先从当前团队最痛的一两个点切入,比如“聚合边界混乱”或者“Repository泄漏”,用技能包把这一个点焊死,跑顺之后再往其他方向扩展。DDD落地是一场持久战,AI给我们的不是一颗银子弹,而是一套可以把架构纪律贯彻到每一行代码的“自动巡检系统”。
