主题
界面
主题
微服务拆分回答了「拆的代价」;DDD 回答「按什么拆、拆完每个边界里怎么建模」。
★ 本节把 DDD 战术设计做成 5 组可运行实测 ✓✓:
贫血模型:setter 一调 → **「已支付 + 总价 0.01 元 + 3 条明细」的非法状态被构造出来** ✗✗✗
充血聚合:金额不足 / 已支付改明细 / 超 5 条明细 → **全部被聚合根拒绝** ✓✓✓
值对象 :跨币种相加直接抛异常、运算返回新对象 ✓✓
领域事件:先发事件后回滚 = 「幽灵订单」✗✗;提交后发布 = 安全 ✓✓
限界上下文:订单与库存各自建模 + 防腐层翻译 ✓✓★ 大部分「业务系统」的代码长这样 ✗:
order.setStatus("PAID");
order.setTotalCents(1);
order.setItemCount(3);
// 编译通过、保存成功 —— 一个「不可能」的订单进入了数据库 ✗✗✗★★ 根因 ✓✓:业务规则写在了 Service 里,而模型只是「数据袋子」 —— 只要有一个入口忘了校验,规则就破防 ✗✗;DDD 的答案:把规则放回模型内部 ✓✓✓。
★ 贫血模型实测 ✗✗:
直接 set:状态=PAID,总价=1 分,明细=3 条
→ **已支付、总价 0.01 元、却有 3 条明细** —— 一个不可能的组合被构造出来了 ✗✗✗★ 充血模型实测(同样三个非法操作,全部被拒) ✓✓✓:
支付 40 元(应付 50 元)→ 被拒:支付金额不足:应付 5000 分,实付 4000 分 ✓✓
已支付后加明细 → 被拒:订单 9527 状态为 PAID,不能改明细 ✓✓
第 6 条明细 → 被拒:单笔订单最多 5 条明细 ✓✓★★ 一句话 ✓✓✓:贫血模型把「规则」变成「纪律」,充血模型把规则变成「物理约束」 —— 纪律会松,物理约束不会 ✓✓。
a = 10.00 CNY,b = 10.00 CNY → a.equals(b) = true ✓(按值相等,不是按引用)
a.plus(c) = 15.00 CNY,而 a 仍然是 10.00 CNY ✓✓(运算返回新对象,原对象不变)
跨币种相加 → 直接抛异常:币种不同,不能相加:CNY vs USD ✓✓★ 什么时候用值对象 ✓:「没有身份、只关心值」的概念 —— 钱、日期区间、地址、坐标、颜色 ✓; 「有生命周期、要区分个体」的才用实体(订单、用户、设备)✓。
★ 判断技巧 ✓✓:问「两个对象可不可以互换」 —— 两张 10 元钞票可以互换(值对象); 两个订单号 9527 与 9528 不能互换(实体)✓。
★ 聚合的三条设计准则 ✓✓✓:
| 准则 | 含义 | 实测对应 |
|---|---|---|
| 规则内聚 | 违反不变量的操作无法通过外部调用完成 | 三个非法操作全被聚合根拒绝 ✓✓ |
| 一次事务只改一个聚合 | 跨聚合的一致性靠领域事件 + 最终一致 | 订单聚合 → 事件 → 库存命令 ✓ |
| 聚合要小 | 只保护「必须立刻一致」的部分 | 订单项是内部实体,不单独建聚合 ✓ |
★★ 为什么「小聚合」是关键 ✓✓✓:聚合是加锁的粒度 —— 大聚合 = 高冲突(并发都抢一把锁)+ 长事务 ✗✗; 把「可最终一致」的部分移出聚合,系统才能并发起来 ✓✓(呼应 《高并发系统设计》 的并发设计)。
-- 错误做法:先发事件、后提交事务 --
事件已发出:[OrderPaid(order=9530, amount=1000分)]
事务回滚(如后续步骤失败)→ 下游已收到「订单已支付」,但订单实际不存在 ✗✗✗
-- 正确做法:事务提交后再发布 --
事务提交 ✓ → 发布:[OrderPaid(order=9531, amount=1000分)] ✓✓★★ 正确姿势 ✓✓✓:聚合内先「收集」事件 → 本地事务提交成功 → 再「发布」(进程内发布 + 事务后投递); 下游消费必须幂等 —— 事件可能重复投递(呼应 《分布式事务》 的四件套)✓✓。
订单上下文的「商品」:OrderContextItem[sku=sku-1, priceCents=3000, snapshotName=机械键盘](关心价格快照)
库存上下文的「商品」:InventoryContextItem[sku=sku-1, stock=42, safetyStock=5](关心库存量)
防腐层把「订单已支付」翻译成:InventoryCommand[sku=sku-1, quantity=1, reason=订单支付成功 → 扣减库存]★★ 核心原则 ✓✓✓:同一个词(商品)在不同上下文里是「不同的模型」 —— 强行共用一个「大而全」模型 = 改一处崩两处 ✗✗; 各自建模 + 防腐层(ACL)转换 = 上下游独立演进 ✓✓。
★ 战略设计的产物 ✓:限界上下文边界 + 上下文映射(谁调谁、怎么翻译)—— 这才是拆微服务的「业务依据」✓✓。
void addItem(String sku, long cents) {
if (status != CREATED) throw new IllegalStateException("状态为 " + status + ",不能改明细");
if (items.size() >= MAX_ITEMS) throw new IllegalStateException("单笔订单最多 " + MAX_ITEMS + " 条明细");
items.add(new OrderItem(sku, cents));
}★ 注意 ✓✓:聚合的方法「要么成功、要么抛异常」,没有中间态 —— 校验通过才改状态 ✓。
record Money(long cents, String currency) {
Money plus(Money other) { requireSameCurrency(other); return new Money(cents + other.cents, currency); }
}Order 聚合里直接改 inventory.stock —— 两个聚合被一个事务锁住 ✗✗
正解:订单提交后发 OrderPaid 事件,库存上下文自己扣(最终一致)✓✓代码:labs/java-deep/src/main/java/cn/kb/learn/deep/architecture/DddTacticsLesson.java
实测输出(节选):
===== 1. 贫血模型:规则不在模型里,就守不住 =====
直接 set:状态=PAID,总价=1 分,明细=3 条
→ **已支付、总价 0.01 元、却有 3 条明细** —— 一个不可能的组合被构造出来了 ✗✗✗
===== 3. 聚合根:不变量在模型内部,不靠调用方自觉 =====
支付 40 元(应付 50 元)→ 被拒:支付金额不足:应付 5000 分,实付 4000 分 ✓✓
已支付后加明细 → 被拒:订单 9527 状态为 PAID,不能改明细 ✓✓
===== 4. 领域事件:发布时机决定「幽灵订单」会不会发生 =====
-- 错误做法:先发事件、后提交事务 --
事务回滚(如后续步骤失败)→ 下游已收到「订单已支付」,但订单实际不存在 ✗✗✗
-- 正确做法:事务提交后再发布 --
事务提交 ✓ → 发布:[OrderPaid(order=9531, amount=1000分)] ✓✓MAX_ITEMS 从 5 改成 10:观察聚合是唯一需要改的地方 ✓✓;BorrowRecord:借书动作是否由聚合根守护?(逾期、续借次数、状态流转)✓✓✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | 模型只有 getter/setter ✗✗✗ | 规则散落,迟早破防 ✓✓ |
| 2 | 用一个事务改多个聚合 ✗✗ | 锁冲突 + 长事务;跨聚合用事件 ✓✓ |
| 3 | 聚合做大(恨不得装下整个领域) ✗✗ | 并发全被一把锁串起来 ✓✓ |
| 4 | 先发事件后提交事务 ✗✗✗ | 幽灵订单 / 幽灵支付 ✓✓ |
| 5 | 两个上下文共用一张表/一个模型 ✗✗ | 改一处崩两处 ✓✓ |
| 6 | 把 DDD 当技术(只搬代码结构) ✗ | DDD 的第一产物是边界与语言,不是目录 ✓✓ |
DddTacticsLesson 五段实验 ✓✓✓;BorrowRecord.borrow()/renew()/returnBook() 守护规则 ✓✓✓。验收方式:给一个业务概念,能判断它是实体还是值对象、聚合边界画在哪、哪些规则要内聚、跨聚合怎么同步。
扩展任务:为项目 A 写一页《领域模型说明》:上下文边界、聚合清单(含不变量)、领域事件清单与消费方 ✓✓
《服务网格》 服务网格 —— 从「代码内的模式」转向「基础设施里的能力」。
暂无