领域驱动设计(DDD)入门:限界上下文、实体、值对象、聚合根在交易系统中的映射
说实话,很多团队做微服务改造,第一步就栽了。
为什么?因为他们把微服务拆分当成了「按功能模块切代码」。结果呢?服务拆了一堆,但改一个需求要改七八个服务,比原来还痛苦。
我当年带团队做交易系统重构时,也踩过这个坑。后来痛定思痛,老老实实学了DDD。说白了,DDD就是教你怎么把业务边界划清楚,让每个服务各管各的一亩三分地。
限界上下文:微服务的「边界感」
限界上下文这个概念,我理解起来花了点时间。后来想通了——它就是微服务的「边界」。
你想想看,一个交易系统里,「订单」这个词在不同场景下含义完全不同:
- 下单时,订单包含商品、价格、收货地址
- 支付时,订单关心的是金额、支付状态
- 物流时,订单关注的是发货状态、快递单号
如果用一个「订单模型」去覆盖所有场景,那这个模型就会变得臃肿不堪。我在项目中见过一个Order对象,里面塞了80多个字段,一半以上是null。
交易系统中典型的限界上下文划分:
| 限界上下文 | 核心职责 | 典型模型 |
|---|---|---|
| 订单上下文 | 订单创建、状态流转 | 订单、订单项、收货地址 |
| 支付上下文 | 支付处理、退款 | 支付单、交易流水 |
| 库存上下文 | 库存扣减、预占 | 库存、库存流水 |
| 物流上下文 | 发货、配送 | 运单、物流轨迹 |
每个上下文都有自己的语言——这叫「通用语言」。团队内部沟通时,就用这套语言。比如订单上下文的人说「下单」,支付上下文的人说「支付」,各说各话,但通过接口对齐。
实体:有「身份证」的对象
实体是什么?简单说:有唯一标识,且会变化的对象。
在交易系统里,最典型的实体就是订单。每个订单有一个订单号,这个号从创建到完结都不会变。但订单的状态、金额、收货地址都会变。
// 订单实体
public class Order {
private OrderId orderId; // 唯一标识,不变
private OrderStatus status; // 状态,会变
private Money totalAmount; // 金额,会变
private List<OrderItem> items; // 订单项,会变
// 行为:修改状态
public void changeStatus(OrderStatus newStatus) {
// 状态校验逻辑
this.status = newStatus;
}
}
判断一个对象是不是实体,就看它有没有「身份」——两个对象即使属性一模一样,只要ID不同,就是不同的实体。比如两个用户都叫张三,但ID不同,那就是两个人。
值对象:没有「身份证」,只有「值」
值对象和实体最大的区别:它没有唯一标识,只关心「是什么」,不关心「是谁」。
举个例子:金额。100元和100元,只要数值和币种一样,就是同一个金额。你不需要给每个金额分配一个ID。
// 值对象:金额
public class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// 值对象是不可变的
public Money add(Money other) {
// 币种校验
return new Money(this.amount.add(other.amount), this.currency);
}
// 重写equals和hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.equals(money.amount) && currency.equals(money.currency);
}
}
交易系统里常见的值对象:
- 地址:省、市、区、详细地址——两个地址一样就是一样
- 金额:数值+币种
- 时间段:开始时间+结束时间
- 联系方式:手机号、邮箱
聚合根:数据一致性的「守门员」
聚合根这个概念,我刚开始觉得有点玄乎。后来做交易系统时,被数据不一致的问题折磨得够呛,才真正理解它的价值。
聚合根说白了就是:一组对象的「老大」。外部只能通过聚合根来操作这组对象,不能直接操作内部成员。
在交易系统里,订单就是一个典型的聚合根:
- 订单包含订单项(OrderItem)
- 你不能直接修改订单项的数量,必须通过订单来操作
- 订单负责保证所有订单项的总金额 = 订单总金额
// 聚合根:订单
public class Order {
private OrderId id;
private List<OrderItem> items;
private Money totalAmount;
// 外部只能通过这个方法操作订单项
public void addItem(Product product, int quantity) {
// 校验商品是否存在
// 校验库存
OrderItem item = new OrderItem(product, quantity);
this.items.add(item);
// 重新计算总金额
this.totalAmount = calculateTotal();
}
// 内部方法,外部不可见
private Money calculateTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
聚合根的设计原则:
- 一致性边界:一个事务只修改一个聚合根
- 引用通过ID:聚合根之间通过ID引用,不要直接持有对象引用
- 最终一致性:跨聚合根的操作,用事件驱动实现最终一致
交易系统中的映射实战
好了,理论说完了。咱们看看在交易系统里,这些概念怎么落地。
我画了一张图,帮你理清关系:
从这张图你能看到:
- 订单上下文和支付上下文是独立的限界上下文
- 每个上下文内部有自己的聚合根、实体、值对象
- 上下文之间通过ID引用,不直接持有对象
- 聚合根是外部访问的唯一入口
嗯,DDD的这些概念,说白了就是帮我们把「业务复杂度」转化成「代码结构」。限界上下文划清边界,聚合根守住一致性,实体和值对象各司其职。这套组合拳打下来,交易系统的微服务改造就有了扎实的理论基础。
公众号:蓝海资料掘金营,微信deep3321