3. 领域驱动设计(DDD)回顾
在微服务测试验证中,领域驱动设计(DDD)为代码的组织和测试边界提供了理论基础。理解 DDD 的核心构造块——实体、值对象、聚合和领域服务——是编写高质量单元测试与集成测试的前提。这些概念决定了哪些逻辑应该被独立测试,哪些行为需要作为整体验证。
3.1 核心构造块概览
DDD 的战术模式将领域逻辑封装在明确的构造块中。下表总结了四种核心元素的定义、标识特征和测试关注点:
| 构造块 | 标识特征 | 可变性 | 相等性判断 | 典型测试粒度 |
|---|---|---|---|---|
| 实体 (Entity) | 拥有唯一标识符(ID),生命周期内身份不变 | 可变(状态可更新) | 按 ID 判断 | 单元测试(状态变更 + 行为) |
| 值对象 (Value Object) | 描述性属性,无独立标识 | 不可变(Immutable) | 按所有属性值判断 | 单元测试(构造 + 比较 + 行为) |
| 聚合 (Aggregate) | 实体 + 值对象的边界簇,有根实体(Aggregate Root) | 通过根实体控制变更 | 按根实体 ID 判断 | 集成测试(事务边界 + 一致性) |
| 领域服务 (Domain Service) | 无状态,协调多个聚合或外部资源 | 无状态(Stateless) | 无(单例/函数) | 单元测试 + 集成测试(依赖注入) |
3.2 实体 (Entity)
实体是具有唯一标识且会经历状态变化的对象。在 Go 中,实体通常是一个结构体,其 ID 字段作为身份锚点。测试实体的核心是验证其行为方法是否正确修改了内部状态。
Go 实体示例:
type Order struct {
ID string
items []OrderItem
status OrderStatus
createdAt time.Time
}
func (o *Order) AddItem(item OrderItem) error {
if o.status != OrderStatusPending {
return ErrOrderNotModifiable
}
o.items = append(o.items, item)
return nil
}
func (o *Order) Submit() error {
if len(o.items) == 0 {
return ErrOrderEmpty
}
o.status = OrderStatusSubmitted
return nil
}
实体测试要点:
- 身份测试:验证两个具有相同 ID 的实体被视为同一个对象。
- 状态变更测试:调用行为方法后,检查实体的内部字段是否符合预期。
- 不变式测试:验证实体在非法操作下(如空订单提交)返回错误,状态保持不变。
3.3 值对象 (Value Object)
值对象描述领域的某个度量或描述,没有概念上的身份。它们是不可变的,通过属性值进行比较。在 Go 中,值对象通常不暴露指针接收器,而是返回新实例。
Go 值对象示例:
type Money struct {
amount decimal.Decimal
currency string
}
func NewMoney(amount decimal.Decimal, currency string) (Money, error) {
if amount.IsNegative() {
return Money{}, ErrNegativeAmount
}
if currency == "" {
return Money{}, ErrInvalidCurrency
}
return Money{amount: amount, currency: currency}, nil
}
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, ErrCurrencyMismatch
}
return NewMoney(m.amount.Add(other.amount), m.currency)
}
值对象测试要点:
- 不可变性测试:确保所有“修改”操作返回新实例,原实例不变。
- 相等性测试:验证两个属性完全相同的值对象相等。
- 工厂方法测试:验证构造时的校验逻辑(如金额不能为负)。
3.4 聚合 (Aggregate)
聚合是一组相关对象的集群,外部只能通过聚合根(Aggregate Root)访问内部成员。聚合保证了事务一致性边界:在一个聚合内部的所有变更必须在一次事务中完成。
聚合设计原则:聚合应该尽量小,只包含必须保持强一致性的对象。跨聚合的最终一致性通过领域事件实现。
下图展示了一个典型的聚合结构,其中 Order 作为聚合根,管理内部的 OrderItem 实体和 Money 值对象:
聚合测试要点:
- 事务一致性测试:验证聚合根的操作要么全部成功,要么回滚到一致状态。
- 边界防护测试:确保外部代码无法直接修改聚合内部的对象引用。
- 仓储集成测试:验证聚合的持久化和重建过程。
3.5 领域服务 (Domain Service)
当某个领域行为无法自然地归属于某个实体或值对象时,应将其建模为领域服务。领域服务是无状态的,它协调多个聚合或调用外部接口来完成一个领域操作。
Go 领域服务示例:
type PricingService struct {
discountPolicy DiscountPolicy
taxCalculator TaxCalculator
}
func (s *PricingService) CalculateFinalPrice(order *Order) (Money, error) {
subtotal := order.CalculateSubtotal()
discount := s.discountPolicy.Apply(order)
afterDiscount, _ := subtotal.Subtract(discount)
tax := s.taxCalculator.Calculate(afterDiscount)
return afterDiscount.Add(tax)
}
领域服务测试要点:
- 依赖注入测试:通过接口注入 mock 或 stub,隔离测试领域服务的协调逻辑。
- 无状态验证:确保多次调用相同方法,在相同输入下返回相同结果。
- 集成测试:验证领域服务与真实仓储或外部服务的协作。
常见误区:不要将领域服务当作“万能垃圾桶”。如果一个方法只操作一个实体的私有数据,它应该属于该实体的方法,而不是领域服务。领域服务只处理跨聚合或需要外部资源的逻辑。
3.6 构造块在测试中的角色
下图展示了 DDD 构造块如何映射到不同的测试层次。理解这种映射关系,有助于在微服务项目中合理分配测试资源:
3.7 本章小结
DDD 的四个核心构造块为微服务的代码组织和测试策略提供了清晰的指导:
- 实体和值对象是领域模型的基本单元,适合编写细粒度的单元测试。
- 聚合定义了事务一致性边界,是集成测试的核心关注点。
- 领域服务封装了跨聚合的协调逻辑,测试时需关注依赖注入和外部协作。