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图展示了聚合的结构与边界:
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图展示了四者在一次业务操作中的协作关系:
2.6 设计决策:何时使用实体 vs 值对象?
在建模过程中,区分实体和值对象是常见的设计决策。以下判断标准可供参考:
| 判断问题 | 倾向实体 | 倾向值对象 |
|---|---|---|
| 对象是否需要独立跟踪其生命周期? | 是 | 否 |
| 两个对象属性相同,是否视为同一个? | 否(标识不同则不同) | 是(属性相同则相同) |
| 对象是否会被独立修改? | 是 | 否(通常整体替换) |
| 对象是否需要跨聚合共享? | 谨慎(通常通过标识引用) | 可以安全共享(不可变) |
实战建议:在事件溯源架构中,优先使用值对象描述状态,使用实体管理生命周期,使用聚合定义一致性边界,使用领域事件记录所有状态变更。这种组合能够最大化事件溯源的可追溯性和状态恢复能力。