在Java中如何设计领域对象模型_Java业务建模解析

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 c

annot be cancelled"); } this.status = OrderStatus.CANCELLED; } }

关键点:

  • changeShippingAddress()cancel() 是命令方法,不是 setter;它们隐含前置条件与副作用
  • 字段保持 private,不暴露 setStatus() 这类危险接口
  • 构造函数应强制满足基本不变量(如 orderId 非空、创建时间必设)

值对象(Value Object)要不可变且重写 equals/hashCode

MoneyAddressPhoneNumber 这类对象,本质是“值”,不是“身份”。Java 中常见错误是把它做成可变类,或直接用 String 代替。

例如:Address 若可变,两个订单共用同一 Address 实例,一处改了街道,另一处也跟着变——这不是业务事实,是 bug。

正确实现要点:

  • 所有字段 final,构造后不可变
  • 不提供 setter,也不暴露可变集合(如返回 Collections.unmodifiableList()
  • 必须重写 equals()hashCode(),基于字段内容比较(IDE 可自动生成)
  • 推荐使用记录类(record)简化: public record Address(String street, String city, String zipCode) {}

聚合根(Aggregate Root)要控制边界和一致性

一个常见误区是把整个订单系统建模为单个大聚合:把 OrderOrderItemPaymentLogEntry 全塞进一个 @Aggregate 注解里。结果是并发更新冲突频繁、持久化性能差、事务太长。

聚合根的核心职责是:维护其内部实体/值对象之间的一致性约束,并对外提供唯一访问入口。

例如:

  • Order 是聚合根,OrderItem 是其内部实体,生命周期依附于订单
  • Payment 应是独立聚合,有自己 ID 和生命周期;订单只持有一个 paymentId 引用,而非嵌入整个 Payment 对象
  • 跨聚合的操作(如“支付成功后更新订单状态”)必须通过领域事件或应用层协调,不能在 Order 内部直接调用 payment.confirm()

否则会模糊边界,导致测试困难、数据库耦合、分布式事务陷阱。

JPA/Hibernate 不是领域模型的默认载体

直接用 @Entity 标记领域对象,很快会遇到矛盾:

  • JPA 要求无参构造函数,但领域对象应通过有参构造保证合法性
  • @OneToMany 加载策略常导致 N+1 查询,而领域层不该暴露这种技术细节
  • 为了映射方便加 @Transient@JsonIgnore,污染了领域语义

更稳健的做法是分层隔离:

  • 领域层定义纯净的 OrderOrderItem,不含任何 JPA 注解
  • 持久化层用单独的 OrderJpaEntity 映射表结构,由仓储(OrderRepository)负责在两者间转换
  • 转换逻辑放在仓储实现内,而非领域对象中(避免引入 javax.persistence 包)

这样,当你需要换数据库、加缓存、切分微服务时,领域模型本身不受影响。

最难的不是写出符合 DDD 术语的类,而是每次加一个字段、改一个方法时,都问一句:这个改动是否改变了业务含义?它是否破坏了某个不变量?有没有人会在别处绕过这个逻辑?