先解决"看不懂"的问题: DDD到底把代码拆哪去了
你看不懂 DDD,多半是因为:三层架构里一个 Service 方法干的活,然而DDD被拆进了 x 个不同文件。先背这张对照表,后面读代码就靠它定位:
| 请求处理的活 | 三层架构放在哪 | DDD放在哪 |
|---|---|---|
| 接 HTTP 参数 | Controller | SaleOrderCommonRuntimeController(还是 Controller,只是更薄) |
| 流程编排、加锁、查单 | Service 方法前半段 | OrderBaseSalesRuntimeContext(应用层) |
| 业务规则(能不能退、退多少) | Service 方法中间的 if | OrderBasedSalesEntity(实体的方法) |
| 状态怎么流转 | Service 里的 setStatus |
OrderDescriptorLoader(状态机声明) |
| 事件之后干具体事 | Service 方法后半段 | handler 包(SalesOrderRefundHandler 等) |
| 落库、记日志 | Service 方法最后几行 | SalesOrderDefaultHandler 的 postProcess |
| SQL | Mapper / XML | repo 包(IOrderEntityRepo) |
接下来两节,每一节都按"三层版怎么写 → DDD 版怎么写"完整走一遍。
场景一:发起退款
用户在小程序点"申请退款",传订单号和退款金额,要求:已全额退过的拒掉、有在途退款单的拒掉、退款总额不能超过订单总额。
1.1 三层版:一个 Service 方法全包
按 patient 模块 OrderServiceImpl 的现有风格还原(示意写法,非逐字拷贝):
java
// ===== Controller:接参数就往下扔 =====
@PostMapping("/refund")
public Result<RefundVO> refund(@RequestBody RefundDTO dto) {
return Result.success(orderService.refundOrder(dto));
}
// ===== Service:所有活都在这一个方法里 =====
@Transactional(rollbackFor = Exception.class)
public RefundVO refundOrder(RefundDTO dto) {
// 1. 参数校验
Validator.validateNotNull(dto.getOrderId(), "订单ID不能为空");
if (dto.getAmount() == null || dto.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new GlobalException("退款金额必须大于0");
}
// 2. 查单
Order order = orderMapper.selectById(dto.getOrderId());
if (order == null) {
throw new GlobalException("订单不存在");
}
// 3. 业务规则:能不能退(注意:下个月退款申请页要复用这段,只能复制)
if (RefundStatusEnum.REFUND == order.getRefundStatus()) {
throw new GlobalException("订单已全额退款,不可再次退款");
}
long pending = orderRefundMapper.selectCount(new QueryWrapper<OrderRefund>()
.eq("order_id", order.getId()).eq("succeed", 0));
if (pending > 0) {
throw new GlobalException("存在在途退款单,请等待处理完成后再试");
}
BigDecimal total = dto.getAmount().add(nvl(order.getRefundAmount()))
.add(unSucceedRefundAmount(order.getId()));
if (total.compareTo(order.getTotalAmount()) > 0) {
throw new GlobalException("退款金额超过订单总额");
}
// 4. 技术细节:分布式锁(和上面的业务规则毫无关系,但挤在一起)
RLock lock = redissonClient.getLock(order.getId() + ":refund");
if (!lock.tryLock(1, 10, TimeUnit.SECONDS)) {
throw new GlobalException("请勿重复提交退款");
}
try {
// 5. 状态变更 + 落库
order.setStatus(OrderStatus.REFUNDING); // ← 全局搜 setStatus 的第 N 处
orderMapper.updateById(order);
OrderRefund refund = new OrderRefund();
BeanUtil.copyProperties(dto, refund);
refund.setSucceed(0);
orderRefundMapper.insert(refund);
// 6. 通知渠道、发消息、记日志......
refundChannelApi.apply(order.getChannelOrderNo(), dto.getAmount());
return buildRefundVO(refund);
} finally {
lock.unlock();
}
}
问题在哪,前面系列讲过了,这里只强调一点:第 3 步那 40 行业务规则,是"订单"的知识,却长在 Service 里。下个月退款申请页要判断"能不能展示申请按钮",你只能复制或者再包一个方法。
1.2 DDD 版:一次 /refund 请求的六站旅行
下面全是 obs 模块的真实代码。按请求走的顺序看:
第 ① 站:Controller ------ 薄到不用看逻辑
java
@Operation(summary = "订单退款")
@PostMapping("/refund")
public Result<RefundDto> refund(@RequestBody RefundParam param) {
try {
return runtimeContext.refund(param); // 只转发
} catch (Exception exception) {
log.error("销售订单退款出现异常.", exception);
return Result.error("系统繁忙.请稍候!");
}
}
第 ② 站:应用层 refund() ------ 只编排,不写规则
这是 OrderBaseSalesRuntimeContext.java的真实方法(我删了部分日志):
java
@Transactional(rollbackFor = {Exception.class, GlobalException.class, RuntimeException.class})
public Result<RefundDto> refund(RefundParam param) {
Result<RefundDto> result = new Result<>();
// 1. 找到订单实体(不是 Order 表对象,是"领域实体")
orderEntityRepo.findOrder(orderId, orderNo, sourceChannel, externalOrderNo)
.ifPresent(orderEntity -> {
// 2. 锁交给适配器,方法体里只写业务
Result<RefundDto> r1 = iDomainLockAdapter.logicWithLock(orderEntity.getOrderLockKey(), () -> {
// 3. 业务规则问实体:"我能退吗?"(规则不在这里!)
Result<String> isOrderRefundable = orderEntity.isOrderRefundable(param);
if (!isOrderRefundable.isSucc()) {
return Result.error(isOrderRefundable.getMessage());
}
// 4. 造一个"退款事件"扔给实体------注意:不是 setStatus
EventModel event = EventModelFactory.INSTANCE.getEventModel(SalesOrderEventConstants.refund);
event.setValueObject(JSONUtil.parseObj(param));
JSONObject object = orderEntity.onEvent(event); // ← 核心就这一行
RefundDto refundDto = new RefundDto();
refundDto.setRefundId(object.getLong("refundId"));
return Result.success(refundDto);
});
result.setData(r1.getData());
});
return result;
}
对比三层版:应用层没有一条 if 是业务规则,它只干四件事------找实体、加锁、问规则、发事件。
第 ③ 站:规则在实体身上 ------ isOrderRefundable
三层版第 3 步那 40 行,在这里长成了实体的方法(OrderBasedSalesEntity.java,真实源码脱敏后):
java
public Result<String> isOrderRefundable(RefundParam refundParam) {
SalesOrder order = this.getValueObject();
// 条件1: 已全额退款,不可再退
if (RefundStatusEnum.REFUND == order.getRefundStatus()) {
return Result.error("订单已全额退款,不可再次退款");
}
// 条件2: 有在途退款单,等处理完再说
long pendingRefundCount = orderRefundRepo.count(Wrappers.<OrderRefund>lambdaQuery()
.eq(OrderRefund::getOrderId, this.getIdentity())
.eq(OrderRefund::getSucceed, RefundSucceedEnum.NO));
if (pendingRefundCount > 0) {
return Result.error("存在在途退款单,请等待处理完成后再试");
}
// 本次退款金额必须大于 0
BigDecimal refundAmount = refundParam.getAmount();
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
return Result.error("退款金额必须大于0");
}
// 条件3: 本次 + 已退 + 未成功退款 不得超过订单总额
BigDecimal unSucceedRefundAmount = orderRefundRepo.list(...).stream()
.map(OrderRefund::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal totalRefund = refundAmount.add(order.getRefundAmount()).add(unSucceedRefundAmount);
if (totalRefund.compareTo(order.getTotalAmount()) > 0) {
return Result.error("本次退款金额加上已退款金额和未成功退款金额超过订单总额");
}
return Result.success();
}
以后不管哪个入口想知道"这单能不能退",都来问这一个方法------这就是"规则长在对象身上"的落地样子,不是概念。
第 ④ 站:状态机 ------ 你只管发事件,状态它来管
应用层那行 orderEntity.onEvent(event) 会先过状态机这关。规则在 OrderDescriptorLoader.java 里声明过:
java
// 服务中 + 退款事件 → 退款中,干这活的是 salesOrderRefundHandler
domainDescriptor.registerStateMachine(running, refund, refunding);
domainDescriptor.registerEventTrigger(running, refund, "salesOrderRefundHandler");
如果订单是 cancelled 状态收到 refund 事件?描述符里没这条转移,verify 直接抛异常------你一行 if 都没写,非法流转就是进不来。
第 ⑤ 站:Handler ------ "退款事件发生了,该干什么"
框架按 salesOrderRefundHandler 这个 Bean 名找到 SalesOrderRefundHandler.java(真实源码全文就这么长):
java
@Component
public class SalesOrderRefundHandler extends SalesOrderDefaultHandler {
@Override
public void postProcess(OrderBasedSalesEntity domain, EventModel event) {
// 从事件里把参数捞出来
RefundParam param = JSONUtil.toBean(event.getValueObject(), RefundParam.class);
// 调实体的业务方法:创建退款单(怎么创建是实体的事)
RefundDto refund = domain.refund(param);
// 交给父类统一做:落库 + 记变更日志
super.postProcess(domain, event);
return JSONUtil.parseObj(refund);
}
}
Handler 的宿命接口长这样(ProcessHandler.java):
java
public interface ProcessHandler<ValueObject, E extends GenericDomain<ValueObject>> {
boolean preProcess(E domain, EventModel event); // 主逻辑前的钩子
void process(E domain, EventModel event); // 主逻辑
JSONObject postProcess(E domain, EventModel event); // 主逻辑后的钩子
}
第 ⑥ 站:父类 postProcess ------ 落库 + 字段级变更日志(这节对你排查问题最有用)
super.postProcess 在 SalesOrderDefaultHandler.java:
java
@Override
public JSONObject postProcess(OrderBasedSalesEntity domain, EventModel event) {
// 1. 持久化领域实体(save 到 sales_order)
SalesOrder salesOrder = orderEntityRepo.persist(domain);
// 2. 对比改动前后的订单,记一条字段级变更日志(带 traceId)
logger(event.getIdentity(), LoggerTypeEnum.UPDATE, oldSalesOrder, salesOrder);
return new JSONObject();
}
注意它记日志的方式:反射对比新旧订单的每个字段,生成 "更新销售订单记录: 退款状态 从 未退款 变更为 退款中 traceId:xxx" 这样的日志存进 order_logger 表。**以后你再排查"订单状态是谁、在哪个事件里改的",不用全局搜 setStatus 了,查这个表就行,每条还带 traceId 能串链路。**这是三层写法给不了的排查体验。
串起来看一次退款请求的完整旅行:
| 步骤 | 文件 | 干什么 |
|---|---|---|
| 1 | SaleOrderCommonRuntimeController | 接参数、转发 |
| 2 | OrderBaseSalesRuntimeContext.refund | 找实体 → 加锁 → 问规则 → 发事件 |
| 3 | OrderBasedSalesEntity.isOrderRefundable | 三条退款规则说了算 |
| 4 | GenericDomain.onEvent → 描述符 verify | 状态机放行/拦截 |
| 5 | SalesOrderRefundHandler.postProcess | 调 domain.refund() 创建退款单 |
| 6 | SalesOrderDefaultHandler.postProcess | 落库 + 变更日志 |
| 7 | 框架 getTargetState | running → refunding,改状态的代码你没写 |
场景二:支付回调
支付渠道(比如微信)异步通知你"这笔支付成功了/失败了"。
2.1 三层版骨架
java
@PostMapping("/payNotify")
public Result<?> payNotify(@RequestBody PayNotifyDTO dto) {
Payment payment = paymentMapper.selectById(dto.getPaymentId());
if (payment.getSucceed() == 1) { // 幂等:成功过的回调直接返回
return Result.success();
}
Order order = orderMapper.selectById(payment.getOrderId());
// 拿锁......
if (dto.isSuccess()) {
payment.setSucceed(1);
paymentMapper.updateById(payment);
if (全部支付成功(order)) {
order.setStatus(OrderStatus.RUNNING); // ← 又一处 setStatus
orderMapper.updateById(order);
// 通知服务层开始服务......
}
} else {
payment.setSucceed(2);
paymentMapper.updateById(payment);
if (全部失败(order)) {
order.setStatus(OrderStatus.NOTHING); // ← 又一处
}
}
return Result.success();
}
"全部支付成功""全部失败"这些判断写在方法里,下次要加"部分支付"逻辑就在这堆 if 里再套 if。
2.2 DDD 版真实代码
应用层 paymentNotify 的骨架(和 refund 一个模子:查支付单做幂等 → 找实体 → 加锁 → 选事件 → onEvent):
java
public Result<PaymentNotifyDto> paymentNotify(PaymentResultEventValueObject param) {
OrderPayment payment = orderPaymentRepo.getById(param.getPaymentId());
// 幂等判断:已成功的回调直接返回
if (PaymentSucceedEnum.YES == payment.getSucceed()) {
return Result.success();
}
OrderBasedSalesEntity orderEntity = orderEntityRepo.findOrder(...).get();
return iDomainLockAdapter.logicWithLock(orderEntity.getOrderLockKey(), () -> {
// 按渠道结果"选事件",不是改状态
EventModel event = param.getPaymentResult()
? EventModelFactory.INSTANCE.getEventModel(SalesOrderEventConstants.payment_success)
: EventModelFactory.INSTANCE.getEventModel(SalesOrderEventConstants.payment_fail);
event.setValueObject(JSONUtil.parseObj(param));
orderEntity.onEvent(event);
...
});
}
"支付成功后干什么"在 SalesOrderPaymentSuccHandler.java(真实源码,注意它把业务放在了 preProcess 钩子里):
java
@Override
public boolean preProcess(OrderBasedSalesEntity domain, EventModel event) {
// 1. 从事件里还原参数
PaymentResultEventValueObject valueObject = new PaymentResultEventValueObject();
valueObject.setPaymentId(event.getValueObject().getLong("paymentId"));
valueObject.setOrderId(event.getValueObject().getLong("orderId"));
valueObject.setPaymentResult(event.getValueObject().getBool("paymentResult", true));
// ...其余字段同理
// 2. 调实体的业务方法:更新支付单、算这单是不是全付清了
PaymentNotifyDto dto = domain.paymentSuccessful(valueObject);
// 3. 返回"这单是否已全部支付成功"
return YesNo.YES == dto.getAllSucceed();
}
@Override
public JSONObject postProcess(OrderBasedSalesEntity domain, EventModel event) {
super.postProcess(domain, event); // 照样落库 + 变更日志
PaymentNotifyDto paymentNotifyDto = new PaymentNotifyDto();
paymentNotifyDto.setAllSucceed(YesNo.YES);
return JSONUtil.parseObj(paymentNotifyDto);
}
domain.paymentSuccessful(...) 是实体方法(OrderBasedSalesEntity):更新 OrderPayment 记录、按费用项分摊、判断"是否全部成功/全部失败"、更新订单支付状态------全是支付领域的知识,全在实体里,不在任何 Service 里。
支付这个场景多说一句你熟的:多笔支付单的订单,渠道回调可能来好几次,"是不是全部付清了"这个判断就是 paymentSuccessful 里的领域逻辑,Handler 只负责把结论带回给流程。三层版这个判断散在回调方法的 if 里,DDD 版它有名字、有家、能单测。
两边摆一起看
同一条需求"退款要校验金额",改动位置的差别:
| 你要做的事 | 三层版 | DDD 版(obs) |
|---|---|---|
| 加一条退款规则 | 改 OrderServiceImpl.refundOrder,祈祷别处没复制这段 |
改 OrderBasedSalesEntity.isOrderRefundable,就一处 |
| 改状态流转规则 | 找到所有 setStatus 逐个确认 |
改 OrderDescriptorLoader 里一行声明 |
| 加"医生驳回"功能 | 新方法 + 全局排查状态 | 描述符 2 行 + 一个新 Handler |
| 查"状态被谁改了" | 全局搜代码 + 翻日志 | 查 order_logger 表(字段级 diff + traceId) |
| 单测退款规则 | mock mapper、mock redis、mock api | 直接 new 实体测纯逻辑 |
补充:回头补理论和战略设计(长期)
代码会写了,再读书就不抽象了。推荐顺序:
-
《实现领域驱动设计》(Vaughn Vernon,"红书")------比原著(蓝书)好读得多,先读前半部分;
-
蓝书挑着读:只读"统一语言""限界上下文"两章。
这个阶段的重点是战略设计:新业务接入为什么只需声明描述符?哪些业务根本不该用 DDD?




避坑清单(都是真坑)
-
**别一刀切全上 DDD。**判断标准就一条:这个业务有没有复杂的状态流转和常变规则?没有就别建描述符;
-
别把"充血模型"理解成"实体里什么都能干"。 实体该有的是业务规则,不该有的是 SQL 拼接和远程调用。项目里
OrderBasedSalesEntity偶尔通过 Repo 查退款记录,这是落地妥协------要清楚这是妥协,别把实体写成第二个 Service; -
**别为了事件而事件。**简单同步流程直接方法调用。需要可靠投递的(如支付结果)走消息表 + 补偿,别走轻量事件总线;
-
**先统一语言,再写代码。**和产品对一遍名词表:状态叫什么、事件叫什么、每个词的边界在哪。这一步不花钱,能省一半返工;
-
别背概念面试。 面试官问"聚合根是什么",你用项目里的
GenericDomain+ 订单状态机讲清楚"为什么状态只能从 onEvent 改",比背定义有说服力十倍。
结语
回头看整个系列:DDD 不是一门新技术,而是一种纪律------业务规则集中放、用业务的语言放、放在划清的边界内。三层架构没有错,它只是没有回答"规则放哪"这个问题;DDD 回答了,代价是多写几个类、多学几个概念。
对你这种天天排查支付、缓存、数据库问题的后端,DDD 最实在的价值是:出问题时你要找的代码变少了。 状态不对?先看状态机描述符。退款金额不对?先看实体的 isOrderRefundable 和退款计算方法。规则集中了,排查路径就短了。