ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DDD领域设计模式实战:工单系统代码案例与AI辅助落地

DDD领域设计模式实战:工单系统代码案例与AI辅助落地 简介这套DDD领域设计模式代码案例是一份基于dddsample-1.1.0的完整Java项目面向正在从CRUD开发转向领域建模的后端工程师。案例围绕订单、产品等典型业务场景完整覆盖领域模型、聚合、值对象、实体、服务与仓储等DDD核心概念既示范了各元素的代码组织方式也展示了如何划分聚合边界、保持内部一致性并通过仓储完成持久化。资源包共343个文件以Java源码142个和XML配置119个为主另含HTML页面、JSP视图、properties属性文件、APT格式的架构说明与模式参考以及少量CSS和图片资源压缩包仅687KB体量轻但结构清晰非常适合快速下载研读。其中APT文档梳理了整体架构、模式参考和变更记录可配合代码理解DDD的设计思路。该资源已有7968人学习下载从POJO对象定义到仓储层与ORM框架的整合再到跨聚合的服务编排都能找到对应示例能帮助开发者沉淀一套可复用的DDD实现模板尤其适合需要从理论走向项目实践的Java开发者参考借鉴。 这几年DDD相关的文章满天飞但落到代码层面大多数项目其实是给Controller和Service中间加了一层叫“Domain”的目录就完事。真正困扰人的是领域模型建好了业务规则却不知道该放哪个对象里设计模式背了一大堆真到写聚合根和领域服务时又觉得无处下手。这篇博文不聊理论复习直接用一个工单流转系统的DDD领域设计模式代码案例把工厂、策略、观察者、仓储这些模式在DDD各层里的位置一一点透再结合我用AI写DDD代码的实操经历给出一份能直接照抄的工程骨架。适合正在落地DDD的后端开发者也适合那些被“贫血模型”和“大泥球”折磨得想重构的团队。1. 为什么DDD项目里还要单独谈设计模式1.1 DDD不复杂复杂的是业务规则怎么放DDD本身是建模思想它给你划了限界上下文、聚合、实体、值对象这些概念但没告诉你“一个聚合根在创建时要校验哪些不变量”这种具体动作该怎么写。传统三层架构里业务逻辑集中在Service里规则混成一锅粥DDD把规则下沉到领域层但如果没有设计模式帮忙组织领域层很快又会变成一个大杂烩。其实可以这么理解DDD定的是地皮和房屋结构设计模式是房间里的水电走线。地皮划得再清楚水电走线乱七八糟住进去还是难受。就拿工单处理来说客户提交一个变更请求系统要校验工单类型、计算SLA响应时限、决定自动分派给哪个工程师——这些规则如果全堆在应用服务的一个方法里代码也能跑但下一个人来维护时光是读方法体就要花半天。1.2 设计模式在DDD里的角色是“表达工具”设计模式在DDD里的使用逻辑很清晰它不是装饰而是让领域模型保持完整性和表达力的手段。比如聚合根创建时参数很多、业务校验复杂直接new一个对象会把一堆规则暴露给调用方这时候工厂模式就派上用场同一个业务行为有多种策略实现比如自动分派、手动分派、VIP优先分派策略模式能让领域服务不需要关心具体分支跨聚合的状态联动比如工单完成之后要发通知、更新统计观察者模式可以避免在工单聚合里写死对通知模块的依赖。我用一个表格总结一下典型场景设计模式DDD中的位置解决什么问题工厂模式领域层封装聚合根和复杂对象的创建逻辑保证创建时不变量策略模式领域层动态替换业务规则消除冗长的case分支观察者模式领域层/应用层领域事件发布与订阅跨聚合解耦仓储模式领域层定义接口基础设施层实现隔离持久化细节让聚合专注于业务门面模式应用层统一应用服务入口对领域层多个能力编排2. 选一个业务场景把领域模型立起来2.1 工单系统的业务需求与限界上下文这次拿“客户工单处理系统”开刀这个场景够典型不会有金融系统那样繁琐的外部依赖又能把DDD的核心概念全带出来。业务大概是这样客户提交一个问题工单系统自动识别优先级客服可以手动分派也可以让系统按SLA策略自动分派工程师处理完成后要提交处理结论系统需要记录全过程并发通知如果工单超时未完结还要自动升级。先划限界上下文工单上下文、客户上下文、人员排班上下文、通知上下文。工单上下文是核心域客户和排班是支撑域通知是通用域。每个上下文内部独立建模上下文之间通过领域事件或应用服务协作。这步很重要很多团队建模失败不是因为不会用DDD术语而是全局只有一个模型什么都往里塞最后变成“上帝模型”。2.2 实体、值对象、聚合与领域服务的划分在工单上下文里首先定聚合根WorkOrder。工单的基本属性是工单号、标题、描述、当前状态、优先级、客户、指派人、处理记录列表。这里工单号是标识符是实体的唯一身份但标题、描述这些属性不需要单独身份归为值对象或普通属性。处理记录WorkOrderRecord在工单内部是有唯一标识和时间线的应该是实体但它从属于工单聚合生命周期由工单聚合管理外部不能直接持有它的引用然后修改。值对象用得最自然的是状态枚举比如WorkOrderStatus.CREATED、ASSIGNED、IN_PROGRESS、COMPLETED它没有身份靠属性值相等来比较。优先级也是一个值对象可以带上SLA响应时限和升级时限的计算逻辑。领域服务放在哪像“自动分派工单”这种逻辑涉及分派策略、工程师负载情况既不属于工单本身也不属于某个工程师实体就应该放在领域服务AssignmentService里。它组织策略来决策但核心状态变更仍然落在工单聚合的assignTo(engineer)方法上。这保证了业务规则的位置匹配业务流程的语义。3. 设计模式在DDD各层的落地代码案例3.1 工厂模式封装聚合根的创建过程大多数团队写聚合根就是写一个构造函数然后调用方自己填一堆字段。问题在于聚合根的创建通常伴随不变量约束比如工单创建时必须有标题和客户、初始状态必须是CREATED、优先级不能为空。如果这些规则散落在应用层不同调用方就可能漏掉不同约束。我习惯在领域层定义一个工厂接口再给出实现public interface WorkOrderFactory { WorkOrder createDraft(WorkOrderDraft draft); } Component public class WorkOrderFactoryImpl implements WorkOrderFactory { Override public WorkOrder createDraft(WorkOrderDraft draft) { if (draft.title() null || draft.title().isBlank()) { throw new IllegalArgumentException(工单标题不能为空); } if (draft.customerId() null) { throw new IllegalArgumentException(客户不能为空); } WorkOrderId orderId WorkOrderId.generate(); Priority priority draft.priority() ! null ? draft.priority() : Priority.medium(); WorkOrder workOrder new WorkOrder(orderId, draft.title(), draft.description()); workOrder.init(priority, draft.customerId()); return workOrder; } }注意这里工厂返回的是已经初始化完成的聚合根创建过程涉及生成ID、设置默认值、执行校验。应用层调用时只需要传一个DTO或值对象创建规则全部封装在工厂里。聚合根内部方法尽量保持protected或者对修改关闭避免外部越过工厂和业务方法去改状态。3.2 策略模式让分派规则可插拔工单的分派逻辑很典型小客户普通工单系统自动分派给负载最低的工程师VIP客户工单直接分派给专属支持组大客户还可以配置默认技能要求。如果这些规则都写在AssignmentService里用switch-case去判断客户类型和服务等级那每次新增规则都要改领域服务。策略模式的落地方式是这样定义分派策略接口领域服务持有策略集合按条件选择策略执行。public interface AssignmentStrategy { boolean supports(AssignmentContext context); AssignResult assign(WorkOrder workOrder, AssignmentContext context); }然后分别实现LoadBalancedAssignmentStrategy按工程师负载均分、VipPriorityAssignmentStrategyVIP客户优先、DefaultManualAssignmentStrategy默认人工分派。AssignmentService遍历策略找到第一个supports返回true的策略来执行。这样做的好处是新增分派规则时不需要改已有代码符合开闭原则。策略实现本身可能依赖排班上下文的查询接口那个接口在领域层定义由基础设施层适配。3.3 领域事件与观察者模式跨聚合的轻量协作工单完成这个动作不只是工单内部状态变成COMPLETED那么简单还要触发客户通知、更新SLA监控指标、可能还会创建回访任务。如果WorkOrder.complete()方法直接调用通知服务和指标服务就形成了对其它上下文的强依赖。正确姿势是发布领域事件public class WorkOrder extends AggregateRoot { public CompleteResult complete(String resolution, Operator operator) { // 业务校验只有IN_PROGRESS状态才能完成 this.status WorkOrderStatus.COMPLETED; this.resolution resolution; registerEvent(new WorkOrderCompletedEvent(this.id, this.customerId, operator.id())); return CompleteResult.success(); } }事件发布后由订阅者处理后续动作。可以用Spring的TransactionalEventListener注意事务边界建议在事务提交后发事件避免事务回滚了事件却已经发出去。工单聚合完全没有感知通知模块的存在这就是观察者模式在DDD里的典型价值行为边界清晰回滚风险小。3.4 仓储模式把持久化锁在边界后面仓储可能是DDD里最容易被误解的模式。有人把它当成DAO换个名字继续把查询和更新糊在一起。真正的仓储应该只对聚合根提供持久化能力而且接口定义在领域层实现放基础设施层。领域层不关心数据库是MySQL还是MongoDB也不关心ORM用MyBatis还是JPA。// 领域层定义接口 public interface WorkOrderRepository { WorkOrder findById(WorkOrderId id); void save(WorkOrder workOrder); } // 基础设施层实现 Repository public class JpaWorkOrderRepository implements WorkOrderRepository { private final WorkOrderJpaDao dao; Override public WorkOrder findById(WorkOrderId id) { return dao.findById(id.value()).orElseThrow(() - new WorkOrderNotFountException(id)); } Override public void save(WorkOrder workOrder) { dao.save(WorkOrderEntity.from(workOrder)); } }仓储接口在领域层方向依赖倒置应用层面向接口编程。开始时容易犯的错是仓库接口里写了一堆查询方法比如findByStatusAndPriority、countPendingOrders这会让聚合的持久化接口和数据查询混在一起领域层也被基础设施需求绑架。聚合内实体如果需要批量查询应该单独定义查询接口而不是塞进仓储。4. AI辅助生成DDD代码的实操心得4.1 用AI写DDD代码的提示词思路最近我用AI代码生成工具重写工单模块确实能省不少事。DDD代码的骨架感很强分层、命名、注解都是套路化的这部分AI生成得又快又整齐。但前提是提示词要给得足够具体不能只说“帮我写一个工单模块DDD代码”那样生成出来的一定是教科书里那种到处是泛型、领域事件满天飞、谁都看不懂的花架子。我实际验证下来比较好用的提示词结构是四段式业务背景、限界上下文范围、聚合划分、输出要求。比如基于Spring Boot实现工单模块DDD分层架构。限界上下文包括工单上下文和通知上下文。聚合根是WorkOrder包含状态、优先级、处理记录。请生成domain层的聚合根实体、值对象、仓储接口application层的应用服务interfaces层的controller以及infrastructure层的Jpa实现。领域对象之间通过领域事件解耦。代码风格简洁不使用过度抽象架构。用这个提示词生成的代码基本可以当骨架使用结构是合理的命名也统一。实际跑起来之后发现AI生成代码主要在两处容易出问题一是聚合根方法会写得过于“贫血”大量setter二是应用服务里会直接注入仓储并做事务编排导致应用层又变成事务脚本。这两点需要人工审查。4.2 AI生成代码后的手工审查要点对着AI生成的代码逐层检查我总结了几个最值得盯的地方。首先看领域层有没有setter方法。聚合根不应该暴露setStatus、setAssignedEngineer这类方法状态变更必须通过业务方法表达比如assignTo()、startProcessing()、complete()。如果AI生成了一堆getter/setter说明它把实体当成数据容器了要先修掉。其次看应用服务有没有承载业务规则正确写法应该是一层薄壳只做事务和协调。如果应用服务里有if-else判断工单状态、超过三行计算逻辑赶紧把逻辑下沉到领域服务。最后看依赖方向领域层引入Spring注解或者JPA注解的地方越少越好这些应该属于基础设施层。4.3 一个可抄作业的最小DDD工程目录使用AI辅助之后我整理版工程结构基本固定如下。这个结构适合中大型单体应用微服务边界清晰的场景也适合作为后续拆微服务的底子。workorder-module ├── application │ ├── dto │ ├── service # 应用服务事务和编排 │ └── assembler # DTO和领域对象互转 ├── domain │ ├── model # 实体、聚合根、值对象 │ ├── service # 领域服务 │ ├── factory # 工厂 │ ├── repository # 仓储接口 │ └── event # 领域事件定义 ├── infrastructure │ ├── repository # 仓储实现 │ ├── adapter # 消息、外部API适配 │ └── config └── interfaces ├── controller └── listener # 事件订阅每次生成项目模板时我会先让AI按这个结构建目录再往里面填业务代码。这样能保证模块边界一开始就是清晰的后面改动也大不到哪去。5. 高频踩坑与排查指引5.1 聚合过大与对象图深度工单和客户、操作记录、审批流经常被建模成一个巨大的聚合结果每次加载工单要join十几张表性能差并发冲突也多。DDD聚合应该小粒度设计工单聚合包含工单本身和处理记录客户模型在客户上下文里维护工单里只保存customerId。两个上下文如果需要关联数据通过查询服务处理。批量加载和报表更不应该走聚合仓储单独提供只读查询通道。5.2 依赖方向反了基础设施反向依赖领域层这种情况在团队协作项目里很容易反弹某个同事图方便直接在领域实体上加了Table、Column注解美其名曰“这样代码少”实际上把领域层绑死在具体持久化框架上。检查方法很简单按模块边界看依赖箭头只能从interfaces指向applicationapplication指向domaininfrastructure实现domain接口。如果发现domain层的import里有Spring Data JPA或者MyBatis就是结构松动的信号要尽早拉回来。5.3 常见问题速查表现象可能原因处理建议领域对象全部是getter/setter没有业务行为贫血模型把业务规则移入实体方法先识别业务事件再定方法应用服务方法超过几十行全是if-else业务逻辑泄漏到应用层抽取领域服务或策略应用层只做协调聚合根互相引用、跨聚合修改聚合边界划分错误用领域事件做异步协作或重新划分上下文仓储接口里全是查询方法查询与持久化混为一谈查询走单独读模型仓储只负责聚合持久化领域事件在事务里发回滚后无法撤销事务边界错误使用事务提交后发送事件机制排查时我习惯先统一看一个完整业务链路比如“创建工单-分派-处理-完成-通知”把链路里每一步属于哪个上下文、哪个聚合、哪个应用服务列出来很多结构问题一下子就暴露了。6. 面向长期演进的落地清单这轮重写工单模块我实际的顺序是先圈定限界上下文和聚合再把领域事件和设计模式按场景套进去最后用AI辅助补齐生成代码骨架再逐层审查修复。整个流程下来最深的感受是DDD对团队的要求不在于用多少模式而在于能不能持续守住分层边界。设计模式不是用来炫技的当代码里有多个聚合协作、多个策略分支、多个灾难性的if-else时模式的作用才真正显现。另外还有一个容易被忽略的点刚开始落地DDD时不要一次把整个系统都重写挑一个业务复杂度高、变动频繁的核心模块做试点比如工单、订单、审批这类流程型业务。跑通一个模块团队形成了“业务规则放领域层、应用层只做编排”的肌肉记忆再逐步铺开会顺很多。最后再分享一个小技巧代码评审时候专门让一个人只盯依赖方向和聚合边界不盯具体业务逻辑这个角色能拦住80%的架构腐化问题。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进