一次性讲清DDD设计中的值对象(value object)、实体(entity)和域(domain)

在 DDD(领域驱动设计)中,实体(Entity)值对象(Value Object)域(Domain)是构建领域模型最核心的基石。它们之间的区别主要体现在身份标识生命周期以及抽象层级上。

1. 核心概念与区别

实体(Entity)

  • 核心特征拥有唯一标识(Identity)
  • 含义:即使两个实体的所有属性完全相同,只要它们的唯一标识不同,它们在系统中就被视为两个完全不同的对象。实体通常具有生命周期,其状态会随着时间发生变化。
  • 关注点:它是“谁”(Who)。

值对象(Value Object)

  • 核心特征没有唯一标识,仅通过属性值来相等
  • 含义:它用来描述领域中的某个特征或属性。如果两个值对象的所有属性值都相同,那么它们就是完全等价的,可以相互替换。值对象通常是不可变的(Immutable)
  • 关注点:它是“什么”(What)。

域(Domain)

  • 核心特征业务问题的边界与范围
  • 含义:域是最高层级的抽象,指代软件要解决的业务问题本身。在 DDD 中,它通常被划分为核心域(业务核心竞争力)、支撑域(必要的业务支持)和通用域(通用行业能力)。在战术设计中,域会被进一步划分为限界上下文(Bounded Context),即模型生效的明确边界。
  • 关注点:它是“在哪”(Where/Scope)。

2. 通用场景:电商订单系统(E-commerce Order System)

为了让你更直观地理解,我们以“电商订单系统”为例,看看这三者在实际建模中是如何协同工作的。

场景描述

用户“张三”在平台上购买了一部手机,系统生成了一个订单,并记录了收货地址。

建模设计拆解

① 域(Domain)的划分
在这个电商场景中,我们会将庞大的业务划分为不同的域(限界上下文):

  • 订单域(Order Context):负责订单的创建、支付状态流转、发货等核心业务。
  • 商品域(Product Context):负责商品的上下架、库存扣减。
  • 用户域(User Context):负责用户的注册、登录、实名认证。
    (注:订单域和商品域虽然都有“商品”这个词,但含义不同。订单域关心的是“交易快照”,商品域关心的是“库存管理”,这就是限界上下文的隔离作用。)

② 实体(Entity)的建模
在订单域中,订单(Order)订单项(OrderItem)就是典型的实体。

  • 订单(Order):拥有全局唯一的 OrderId。即使张三两次下了内容完全一样的订单,它们也是两个独立的实体,各自有独立的支付状态和生命周期。
  • 订单项(OrderItem):拥有 ItemId。它依附于订单存在,代表订单中购买的具体商品记录。

③ 值对象(Value Object)的建模
在订单域中,收货地址(ShippingAddress)金额(Money)就是典型的值对象。

  • 收货地址(ShippingAddress):由“省、市、区、街道、收件人、电话”组成。在订单模型中,我们并不关心这个地址有没有一个独立的 AddressId,我们只关心它的。只要这些属性一样,我们就认为它们是同一个地址。且订单一旦生成,这个地址就不应该被随意修改(不可变性)。
  • 金额(Money):由“数值(Amount)”和“币种(Currency)”组成。100元人民币和100元人民币是完全等价的,无需关心它的标识。

3. 总结对比

维度 实体 (Entity) 值对象 (Value Object) 域 (Domain)
本质 业务对象本身 对象的属性/特征 业务问题的边界
标识 必须有唯一标识 (ID) 无标识,靠属性值判等 限界上下文边界
可变性 可变,有生命周期 不可变 (Immutable) 随业务演进
举例 订单、用户、商品 地址、金额、颜色、坐标 订单域、支付域、用户域

在实际开发中,尽量多使用值对象,少滥用实体。将原本散落在实体中的基础属性(如地址、时间范围、金额)提取为值对象,不仅能提高代码的复用性,还能让领域模型更加纯粹和易于维护。

4 补充

4.1 业务流转中的 DDD 核心原则体现

在这个完整的下单场景中,有几个非常关键的 DDD 设计原则:

  1. 跨域只通过 ID 关联(不直接引用对象)

    • 错误做法:在 Order 实体中直接注入一个 User 对象和一个 Product 对象。这会导致订单域和用户域、商品域在代码和数据库层面强耦合。
    • 正确做法Order 中只保存 user_idproduct_id。当需要展示张三的名字或商品图片时,由应用层(Application Service)查询服务(CQRS)去用户域和商品域分别查询并组装。
  2. 值对象的“快照”特性(不可变性)

    • 张三下单时,Order 里的 ShippingAddress 是从 UserUserAddress 复制过来的一个新值对象
    • 如果第二天张三搬家了,修改了 UserAddress已经生成的 Order 里的 ShippingAddress 绝对不会受影响。这就是值对象不可变性在业务上的巨大价值。
  3. 聚合根的边界控制

    • Order 是交易域的聚合根(Aggregate Root)。外部系统(如支付域)只能通过 order_id 来操作订单(例如调用 order.pay()),绝对不能直接去修改订单内部的 OrderItem 数量或 ShippingAddress。所有的修改必须通过聚合根暴露的业务方法来进行,以保证业务规则的一致性。
  4. 支付域的独立性

    • 支付成功后,支付域产生 PaymentRecord,并通过领域事件(Domain Event)(如 PaymentCompletedEvent)通知交易域。交易域监听到事件后,再调用 Order.pay() 更新订单状态。这避免了支付域直接修改订单表,实现了域与域之间的解耦。

4.2 领域事件

在没有领域事件的传统架构中,支付成功后,支付服务往往会直接调用订单服务的接口(如 orderService.updateStatusToPaid(orderId)),或者更糟的是,直接跨库去修改订单表。这会导致支付域强依赖于交易域。如果订单服务宕机,支付流程也会失败。

引入领域事件后,架构变成了异步、单向的发布-订阅模式。以下是完整的解耦过程:

1. 支付域:产生并发布事件

当用户完成支付,第三方支付网关回调成功后,支付域在自己的边界内完成业务:

  • 创建或更新 PaymentRecord 实体,状态变为 SUCCESS
  • 触发事件:在同一个本地事务中,将一条领域事件(如 PaymentCompletedEvent)写入数据库的事件表(Outbox 模式,保证可靠性),或者发送到消息队列(如 Kafka/RabbitMQ)。
  • 事件内容通常包含:payment_id, order_id, paid_amount, pay_time

关键点:支付域不知道交易域的存在,它只负责声明“支付已经完成了”这个事实。

2. 交易域:订阅并处理事件

交易域作为一个独立的消费者,监听支付成功的事件:

  • 监听到 PaymentCompletedEvent 后,提取出 order_id
  • 调用订单聚合根的业务方法:order.pay(paymentId, payTime)
  • 订单内部校验状态(比如不能重复支付),然后将 OrderStatusCREATED 变更为 PAID,并持久化。

关键点:交易域不知道支付域是如何完成支付的(是支付宝、微信还是银行卡),它只关心“订单被支付了”这个结果。

3. 解耦带来的三大核心价值

  1. 代码与架构解耦
    支付域的代码里没有任何 import TradeDomainOrderService。两个域在物理和逻辑上完全隔离,可以分配给不同的团队独立开发、独立部署。
  2. 容错性与高可用(最终一致性)
    如果交易域因为网络抖动或数据库死锁暂时不可用,支付域的流程不会受到任何影响。消息队列会不断重试投递 PaymentCompletedEvent,直到交易域恢复正常并成功处理。这实现了分布式系统中的最终一致性
  3. 扩展性(一对多广播)
    如果未来业务增加,支付成功后不仅要更新订单状态,还要增加用户积分触发短信通知生成发票
    • 传统做法:修改支付域代码,加上积分服务、短信服务、发票服务的调用(严重违反开闭原则)。
    • 领域事件做法:支付域完全不用改代码。只需要积分域、通知域、发票域分别去订阅同一个 PaymentCompletedEvent 即可。

4. 流程图示

[支付域]                      [消息队列/事件总线]             [交易域]
   |                                   |                          |
   |-- 1. 接收支付回调                 |                          |
   |-- 2. 更新 PaymentRecord          |                          |
   |-- 3. 发布 PaymentCompletedEvent -|-> 4. 路由事件             |
   |                                   |                          |-- 5. 接收事件
   |                                   |                          |-- 6. 查找 Order
   |                                   |                          |-- 7. 调用 order.pay()
   |                                   |                          |-- 8. 更新 OrderStatus

通过这种设计,DDD 不仅规范了代码结构,更从架构层面保证了系统的弹性和可维护性。

4.3 聚合根

聚合根(Aggregate Root)是实体的一种特殊形式,它与其他概念的关系存在严格的层级逻辑:

域(Domain)
└── 聚合(Aggregate) → 由**聚合根**作为唯一入口点
    └── 聚合根(Aggregate Root) → 一种**特殊实体**(具有全局唯一ID + 事务边界控制权)
        ├── 内部实体(Entity) → 仅在聚合内有唯一标识
        └── 值对象(Value Object) → 无ID、不可变、描述性属性

关键区别

  • 普通实体:可能属于某个聚合内部(如 OrderItem),不能独立存在
  • 聚合根:是对外暴露的实体,是跨域交互的唯一入口(如 Order
  • 值对象:永远依附于实体存在,不能脱离宿主独立存在
  • :是业务能力的边界,聚合是域内的最小事务单元

4.3.1. 聚合根的核心职责(以 Order 为例)

特性 说明 订单场景示例
全局唯一ID 对外暴露的标识符,其他域仅通过此ID引用 支付域的 PaymentRecord 只存 order_id绝不持有 Order 对象
事务边界控制 聚合内所有对象必须原子性修改 添加 OrderItem + 修改 total_amount 必须在同一事务中完成
业务规则守护者 所有状态变更必须通过聚合根方法,禁止外部直接修改内部对象 不能 order.status = PAID,必须 order.pay(paymentId)(内部校验支付一致性)
跨域交互入口 其他域只能通过聚合根ID发起请求,不能穿透到内部实体 支付成功后,支付域只能调用 orderService.pay(order_id),不能操作 OrderItem

4.3.2. 聚合内部结构规则

  • Order 聚合内部

    • 允许:Order 直接持有 ShippingAddress(值对象,组合关系)
    • 允许:Order 直接持有 OrderItem(内部实体,组合关系)
    • 禁止:OrderItem 直接持有 Product 对象(必须通过 product_id 弱依赖)
    • 禁止:外部服务直接修改 OrderItem.quantity(必须通过 Order.updateItem()
  • 跨聚合规则

    张三下单时选择地址的完整路径
    User 聚合 → 提供 UserAddress应用层复制为 ShippingAddress → 传入 Order.create()
    关键点Order 不直接引用 UserAddress,而是创建自己的值对象快照

4.3.3. 为什么 PaymentRecord 是聚合根?

  • 支付记录有独立生命周期:支付可能失败重试,而订单状态已锁定
  • 支付域不依赖交易域:支付成功后,PaymentRecord.complete() 仅发布事件,不调用订单服务
  • 事务边界分离:
    支付域事务:更新 PaymentRecord + 发布 PaymentCompletedEvent → 1PC 本地事务
    交易域事务:接收事件 + 更新 Order → 独立的 1PC 本地事务
    

4.3.4. 值对象的不可变性实战

当张三在下单后修改了用户地址:

// 用户域:修改地址(创建新值对象)
User user = userRepository.findById("zhangsan");
user.updateAddress(newAddress); // 旧 UserAddress 仍存在于历史订单中

// 交易域:订单内的 ShippingAddress 永远是下单时的快照
Order order = orderRepository.findById("ORDER123");
order.getShippingAddress().getCity(); // 仍是"北京市"(下单时的值)

四、错误设计 vs 正确设计对比

反模式:跨域强引用

// 错误!订单直接持有用户对象(导致交易域强依赖用户域)
class Order {
  private User user; // 灾难性设计!
}

正确模式:ID 弱依赖 + 事件解耦

// 交易域 Order 聚合根
class Order {
  private String userId; // 仅存储ID
  
  // 通过领域事件响应支付结果
  public void handlePaymentCompleted(PaymentCompletedEvent event) {
    if (this.status == CREATED) {
      this.status = new OrderStatus(PAID);
      this.paymentId = event.getPaymentId();
    }
  }
}

// 支付域 PaymentRecord
class PaymentRecord {
  public void complete() {
    this.status = SUCCESS;
    // 仅发布事件,不调用订单服务
    domainEventPublisher.publish(new PaymentCompletedEvent(orderId, paymentId));
  }
}

当张三支付时遇到以下场景:

问题场景 传统强耦合架构 DDD 聚合+领域事件架构
支付成功但订单服务宕机 支付流程卡死,用户需重试 支付域正常完成,事件异步重试
张三修改地址后退货 退货单可能错误关联新地址 退货单基于订单的 ShippingAddress 快照
大促时商品超卖 全局锁库存导致系统雪崩 Product 聚合内控制库存扣减

本质:聚合根是 DDD 中业务一致性的最小单元,通过严格边界隔离了复杂度。在电商系统中,Order 聚合保证了“订单创建-支付-发货”过程的状态一致性,而无需锁住整个数据库。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容