2. DDD 基础回顾:实体、值对象、聚合、聚合根、领域服务、仓储模式
在深入支付订单聚合的实战之前,必须重新校准 DDD 的核心战术模式。支付领域对数据一致性、不可变性和边界划分要求极高,任何概念偏差都会导致后续的聚合设计出现结构性缺陷。本节将结合支付场景,逐一拆解六个核心概念。
2.1 实体(Entity)与值对象(Value Object)
实体与值对象的区分是 DDD 建模的起点。实体拥有唯一标识且会经历生命周期变化;值对象描述某个维度的属性,一旦创建便不可变,且没有全局标识。
| 维度 | 实体(Entity) | 值对象(Value Object) |
|---|---|---|
| 标识 | 拥有全局唯一 ID(如 OrderID) | 无全局标识,靠属性值相等判断 |
| 可变性 | 属性可变,状态随时间变化 | 不可变,修改即替换 |
| 生命周期 | 有独立生命周期,可被追踪 | 无生命周期,依附于实体 |
| 支付示例 | 订单(Order)、支付单(Payment) | 金额(Money)、地址(Address)、支付方式(PaymentMethod) |
| 相等判断 | ID 相同即相等 | 所有属性相同即相等 |
支付实战要点: 金额(Money)必须设计为值对象,包含币种和金额数值,且提供加减乘除方法返回新实例。绝不允许在实体中直接使用 float64 或 decimal 裸字段,否则精度和币种校验将散落在各处。
2.2 聚合(Aggregate)与聚合根(Aggregate Root)
聚合是一组相关对象的集群,外部只能通过聚合根访问内部成员。聚合根是聚合中唯一能被外部引用的实体,负责维护整个聚合的不变规则。
在支付场景中,一个典型的聚合是 支付单聚合:聚合根是 Payment,内部包含支付金额(值对象)、支付方式(值对象)、支付流水记录(实体集合)。外部服务只能通过 Payment 根对象发起支付、退款或查询操作。
聚合设计铁律: 一个事务只修改一个聚合。支付单聚合内的所有变更(创建、支付成功、退款)必须通过 Payment 根对象的方法完成,不允许外部直接操作 PaymentRecord 或修改 Money。
2.3 领域服务(Domain Service)
当某个业务逻辑无法自然地归属于某个实体或值对象时,就需要领域服务。领域服务是无状态的,它协调多个聚合或外部资源完成一个领域操作。
支付场景中的典型领域服务:
- 支付路由服务(PaymentRoutingService):根据金额、币种、渠道权重选择最优支付渠道。
- 对账服务(ReconciliationService):对比支付单聚合与渠道侧账单,生成差异记录。
- 风控校验服务(RiskControlService):在支付前检查交易是否触发风控规则。
// 领域服务示例:支付路由选择
type PaymentRoutingService struct {
channelRepo ChannelRepository // 仓储接口
}
// SelectRoute 根据支付单聚合和上下文选择最优渠道
func (s *PaymentRoutingService) SelectRoute(
ctx context.Context,
payment *Payment, // 聚合根
amount Money, // 值对象
) (*ChannelRoute, error) {
// 1. 获取所有可用渠道
channels, err := s.channelRepo.FindAvailable(ctx)
if err != nil {
return nil, fmt.Errorf("获取可用渠道失败: %w", err)
}
// 2. 按权重和金额筛选(领域逻辑)
return rankAndPick(channels, amount)
}
2.4 仓储模式(Repository Pattern)
仓储为聚合提供持久化与重建的接口,使领域层不依赖具体数据库实现。每个聚合根对应一个仓储接口,仓储只负责聚合的完整存取,不暴露内部实体。
| 仓储原则 | 说明 | 支付示例 |
|---|---|---|
| 按聚合根设计 | 每个聚合根一个仓储接口 | PaymentRepository |
| 完整存取 | 一次加载整个聚合,不允许部分加载 | Save(ctx, *Payment) / Load(ctx, paymentID) |
| 接口在领域层 | 接口定义在 domain 包,实现在 infrastructure | domain.PaymentRepository → infra.MySQLPaymentRepo |
| 不暴露内部表结构 | 仓储返回聚合根,不返回数据库行 | FindByOrderID(ctx, orderID) (*Payment, error) |
// 领域层仓储接口定义
package domain
type PaymentRepository interface {
Save(ctx context.Context, payment *Payment) error
Load(ctx context.Context, paymentID PaymentID) (*Payment, error)
FindByOrderID(ctx context.Context, orderID string) (*Payment, error)
Delete(ctx context.Context, paymentID PaymentID) error
}
常见误区: 不要在仓储中暴露内部实体的查询方法,例如
FindPaymentRecordsByStatus()。这会导致领域逻辑泄露到基础设施层,破坏聚合边界。所有对内部实体的操作必须通过聚合根的方法进行。
2.5 六种模式在支付领域的协作关系
下图展示了实体、值对象、聚合、聚合根、领域服务和仓储在支付场景中的完整协作流程:
支付架构决策: 在 Go 中实现 DDD 时,仓储接口返回聚合根指针(
*Payment),内部使用不可变值对象(Money、PaymentMethod)。领域服务通过构造函数注入仓储接口,不依赖具体实现。这种分层确保支付核心逻辑可测试、可替换、可演进。