DDD在Java中落地的核心是让领域对象承载业务语义:封装行为而非仅数据,值对象不可变且重写equals/hashCode,聚合根明确边界并隔离持久化细节。

Domain-Driven Design(DDD)在 Java 中落地,核心不是堆砌注解或套用框架,而是让对象真正承载业务语义。领域对象模型不是数据库表的镜像,也不是 DTO 的变体,它得能回答“这个对象能做什么”“它什么时候是合法的”“它的状态如何被改变”。
领域对象必须封装行为,不能只有 getter/setter
很多团队把 Order 类写成纯数据容器:一堆 private 字段 + public getter/setter。这导致业务逻辑散落在 OrderService 里,Order 自身无法表达“订单可以取消”“已发货的订单不能改地址”这类规则。
正确做法是把校验和状态变更逻辑收进领域对象内部:
public class Order {
private OrderStatus status;
private Address shippingAddress;
public void changeShippingAddress(Address newAddress) {
if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("Cannot modify address after shipment");
}
this.shippingAddress = newAddress;
}
public void cancel() {
if (status == OrderStatus.CANCELLED) return;
if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("Shipped order cannot be cancelled");
}
this.status = OrderStatus.CANCELLED;
}
}
关键点:
-
changeShippingAddress()和cancel()是命令方法,不是 setter;它们隐含前置条件与副作用 - 字段保持
private,不暴露setStatus()这类危险接口 - 构造函数应强制满足基本不变量(如
orderId非空、创建时间必设)
值对象(Value Object)要不可变且重写 equals/hashCode
像 Money、Address、PhoneNumber 这类对象,本质是“值”,不是“身份”。Java 中常见错误是把它做成可变类,或直接用 String 代替。
立即学习“Java免费学习笔记(深入)”;
例如:Address 若可变,两个订单共用同一 Address 实例,一处改了街道,另一处也跟着变——这不是业务事实,是 bug。
正确实现要点:
- 所有字段
final,构造后不可变 - 不提供 setter,也不暴露可变集合(如返回
Collections.unmodifiableList()) - 必须重写
equals()和hashCode(),基于字段内容比较(IDE 可自动生成) - 推荐使用记录类(
record)简化:public record Address(String street, String city, String zipCode) {}
聚合根(Aggregate Root)要控制边界和一致性
一个常见误区是把整个订单系统建模为单个大聚合:把 Order、OrderItem、Payment、LogEntry 全塞进一个 @Aggregate 注解里。结果是并发更新冲突频繁、持久化性能差、事务太长。
聚合根的核心职责是:维护其内部实体/值对象之间的一致性约束,并对外提供唯一访问入口。
例如:
-
Order是聚合根,OrderItem是其内部实体,生命周期依附于订单 -
Payment应是独立聚合,有自己 ID 和生命周期;订单只持有一个paymentId引用,而非嵌入整个Payment对象 - 跨聚合的操作(如“支付成功后更新订单状态”)必须通过领域事件或应用层协调,不能在
Order内部直接调用payment.confirm()
否则会模糊边界,导致测试困难、数据库耦合、分布式事务陷阱。
JPA/Hibernate 不是领域模型的默认载体
直接用 @Entity 标记领域对象,很快会遇到矛盾:
- JPA 要求无参构造函数,但领域对象应通过有参构造保证合法性
-
@OneToMany加载策略常导致 N+1 查询,而领域层不该暴露这种技术细节 - 为了映射方便加
@Transient或@JsonIgnore,污染了领域语义
更稳健的做法是分层隔离:
- 领域层定义纯净的
Order、OrderItem,不含任何 JPA 注解 - 持久化层用单独的
OrderJpaEntity映射表结构,由仓储(OrderRepository)负责在两者间转换 - 转换逻辑放在仓储实现内,而非领域对象中(避免引入
javax.persistence包)
这样,当你需要换数据库、加缓存、切分微服务时,领域模型本身不受影响。
最难的不是写出符合 DDD 术语的类,而是每次加一个字段、改一个方法时,都问一句:这个改动是否改变了业务含义?它是否破坏了某个不变量?有没有人会在别处绕过这个逻辑?










