在 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 设计原则:
-
跨域只通过 ID 关联(不直接引用对象)
-
错误做法:在
Order实体中直接注入一个User对象和一个Product对象。这会导致订单域和用户域、商品域在代码和数据库层面强耦合。 -
正确做法:
Order中只保存user_id和product_id。当需要展示张三的名字或商品图片时,由应用层(Application Service)或查询服务(CQRS)去用户域和商品域分别查询并组装。
-
错误做法:在
-
值对象的“快照”特性(不可变性)
- 张三下单时,
Order里的ShippingAddress是从User的UserAddress复制过来的一个新值对象。 - 如果第二天张三搬家了,修改了
UserAddress,已经生成的Order里的ShippingAddress绝对不会受影响。这就是值对象不可变性在业务上的巨大价值。
- 张三下单时,
-
聚合根的边界控制
-
Order是交易域的聚合根(Aggregate Root)。外部系统(如支付域)只能通过order_id来操作订单(例如调用order.pay()),绝对不能直接去修改订单内部的OrderItem数量或ShippingAddress。所有的修改必须通过聚合根暴露的业务方法来进行,以保证业务规则的一致性。
-
-
支付域的独立性
- 支付成功后,支付域产生
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)。 - 订单内部校验状态(比如不能重复支付),然后将
OrderStatus从CREATED变更为PAID,并持久化。
关键点:交易域不知道支付域是如何完成支付的(是支付宝、微信还是银行卡),它只关心“订单被支付了”这个结果。
3. 解耦带来的三大核心价值
-
代码与架构解耦:
支付域的代码里没有任何import TradeDomain或OrderService。两个域在物理和逻辑上完全隔离,可以分配给不同的团队独立开发、独立部署。 -
容错性与高可用(最终一致性):
如果交易域因为网络抖动或数据库死锁暂时不可用,支付域的流程不会受到任何影响。消息队列会不断重试投递PaymentCompletedEvent,直到交易域恢复正常并成功处理。这实现了分布式系统中的最终一致性。 -
扩展性(一对多广播):
如果未来业务增加,支付成功后不仅要更新订单状态,还要增加用户积分、触发短信通知、生成发票。- 传统做法:修改支付域代码,加上积分服务、短信服务、发票服务的调用(严重违反开闭原则)。
-
领域事件做法:支付域完全不用改代码。只需要积分域、通知域、发票域分别去订阅同一个
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 聚合保证了“订单创建-支付-发货”过程的状态一致性,而无需锁住整个数据库。