2. 领域建模基础:实体、值对象、聚合、领域事件的定义与关系

领域驱动设计(DDD)的核心在于通过模型精确表达业务逻辑。本章将深入剖析构成领域模型的四个基本构造块:实体(Entity)、值对象(Value Object)、聚合(Aggregate)与领域事件(Domain Event)。理解它们的定义、特征及相互关系,是构建健壮、可演化的事件溯源系统的前提。

2.1 实体(Entity):具有唯一标识的领域对象

实体是领域模型中具有连续性和身份标识的对象。即使其属性随时间变化,只要标识不变,它仍然是同一个对象。例如,一个“用户”实体,其用户名、邮箱可以修改,但用户ID始终不变。

核心特征:
  • 唯一标识(Identity):每个实体必须拥有一个不可变的标识符(如UUID、业务主键)。
  • 可变性(Mutability):实体的属性可以改变,但标识不变。
  • 生命周期:实体有明确的创建、修改、删除生命周期。

在代码中,实体通常通过 equals() 和 hashCode() 方法基于标识进行比较,而非属性值。

// 实体示例:用户
public class User {
    private final UserId userId; // 值对象作为标识
    private String name;
    private Email email;

    public User(UserId userId, String name, Email email) {
        this.userId = userId;
        this.name = name;
        this.email = email;
    }

    public void changeEmail(Email newEmail) {
        this.email = newEmail;
        // 可能触发领域事件:UserEmailChanged
    }

    // equals() 和 hashCode() 基于 userId 实现
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return userId.equals(user.userId);
    }

    @Override
    public int hashCode() {
        return userId.hashCode();
    }
}

2.2 值对象(Value Object):描述性、不可变、无标识

值对象用于描述实体的属性或特征,它没有唯一标识,且通常是不可变(Immutable)的。两个值对象如果所有属性值都相等,则被视为同一个对象。例如,“地址”、“金额”、“颜色”都是典型的值对象。

特征 实体(Entity) 值对象(Value Object)
标识 有唯一标识 无标识,由属性值定义
可变性 可变 不可变(推荐)
相等性 基于标识 基于所有属性值
生命周期 独立生命周期 依附于实体,可被替换
副作用 修改自身状态 创建新实例,不修改自身
// 值对象示例:Email
public final class Email {
    private final String value;

    public Email(String value) {
        // 校验逻辑
        if (value == null || !value.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Email email = (Email) o;
        return value.equals(email.value);
    }

    @Override
    public int hashCode() {
        return value.hashCode();
    }
}

2.3 聚合(Aggregate):事务一致性的边界

聚合是一组相关对象的集群,它被当作一个整体进行数据变更。每个聚合都有一个聚合根(Aggregate Root),它是外部访问聚合内部对象的唯一入口。聚合边界定义了事务一致性的范围:在一个聚合内部,所有状态变更必须在一个事务中完成。

设计原则:
  • 聚合根是实体:聚合根必须是实体,拥有全局唯一标识。
  • 边界内强一致性:聚合内部的状态变更必须立即一致。
  • 边界外最终一致性:跨聚合的变更通过领域事件实现最终一致性。
  • 小聚合原则:聚合应尽量小,只包含必须强一致的对象。

以下SVG图展示了聚合的结构与边界:

聚合结构与边界 聚合边界(事务一致性) 聚合根:Order orderId: UUID OrderItem (实体) itemId, productId ShippingAddress (值对象) street, city, zip Money (值对象) amount, currency 外部服务 只能访问聚合根 说明:聚合根(Order)是外部访问的唯一入口,内部实体(OrderItem)和值对象(ShippingAddress、Money)的变更必须通过聚合根完成。

2.4 领域事件(Domain Event):记录已发生的业务事实

领域事件是领域专家关心的、已经发生的事件。它表示业务状态的一次变更,是不可变的事实记录。在事件溯源架构中,领域事件是状态恢复的唯一数据源。

命名规范:领域事件应使用过去时态的动词命名,例如:OrderPlaced、PaymentReceived、UserEmailChanged。事件名称必须清晰表达业务含义。
// 领域事件示例:订单已创建
public class OrderPlaced implements DomainEvent {
    private final UUID eventId;
    private final UUID orderId;
    private final UUID customerId;
    private final Money totalAmount;
    private final Instant occurredOn;

    public OrderPlaced(UUID orderId, UUID customerId, Money totalAmount) {
        this.eventId = UUID.randomUUID();
        this.orderId = orderId;
        this.customerId = customerId;
        this.totalAmount = totalAmount;
        this.occurredOn = Instant.now();
    }

    // Getters...
    @Override
    public Instant occurredOn() {
        return occurredOn;
    }
}

2.5 四者关系:从静态结构到动态行为

实体、值对象、聚合和领域事件共同构成了领域模型的静态结构与动态行为。它们的关系可以概括为:

  • 实体是聚合的骨架,提供标识和生命周期管理。
  • 值对象作为实体的属性或聚合内部的描述性元素,提供类型安全和业务语义。
  • 聚合是事务一致性的容器,确保边界内的实体和值对象状态变更的原子性。
  • 领域事件是聚合状态变更的输出,用于通知其他聚合或外部系统,实现最终一致性。

以下SVG图展示了四者在一次业务操作中的协作关系:

实体、值对象、聚合与领域事件协作关系 领域模型构造块协作流程 外部命令 PlaceOrder 聚合根:Order place() 方法 OrderItem Money 领域事件 OrderPlaced 事件存储 Event Store 协作流程说明 1. 外部命令调用聚合根(Order)的 place() 方法。 2. 聚合根校验内部实体(OrderItem)和值对象(Money)的状态。 3. 聚合根生成领域事件(OrderPlaced),并追加到事件存储。

2.6 设计决策:何时使用实体 vs 值对象?

在建模过程中,区分实体和值对象是常见的设计决策。以下判断标准可供参考:

判断问题 倾向实体 倾向值对象
对象是否需要独立跟踪其生命周期? 是 否
两个对象属性相同,是否视为同一个? 否(标识不同则不同) 是(属性相同则相同)
对象是否会被独立修改? 是 否(通常整体替换)
对象是否需要跨聚合共享? 谨慎(通常通过标识引用) 可以安全共享(不可变)
实战建议:在事件溯源架构中,优先使用值对象描述状态,使用实体管理生命周期,使用聚合定义一致性边界,使用领域事件记录所有状态变更。这种组合能够最大化事件溯源的可追溯性和状态恢复能力。