2. 领域驱动设计(DDD)核心概念:实体、值对象、聚合、领域事件、仓库模式在Go中的实现
领域驱动设计(Domain-Driven Design,DDD)为复杂业务系统的建模提供了战术模式工具箱。在Go语言中实现DDD时,需要结合Go的简洁性与并发特性,将实体、值对象、聚合、领域事件和仓库模式转化为可执行的代码结构。本章聚焦于这些核心概念在Go中的落地方式,为后续的事件溯源与并发回放优化奠定领域模型基础。
2.1 实体(Entity)
实体是具有唯一标识且会随时间改变状态的对象。在Go中,实体通常表示为带有ID字段的结构体,并通过方法封装业务行为。
关键特征: 唯一标识(ID)、可变状态、业务方法封装。
// 用户实体
type User struct {
ID string // 唯一标识
Name string
Email string
CreatedAt time.Time
UpdatedAt time.Time
}
// 业务方法:更新邮箱
func (u *User) UpdateEmail(email string) error {
if !isValidEmail(email) {
return errors.New("invalid email format")
}
u.Email = email
u.UpdatedAt = time.Now()
return nil
}
// 实体比较:基于ID而非字段值
func (u *User) Equals(other *User) bool {
return u.ID == other.ID
}
| 特性 | 说明 | Go实现要点 |
|---|---|---|
| 唯一标识 | 每个实体拥有不可变的ID | 使用string或uuid类型,构造函数中生成 |
| 可变性 | 属性可通过方法修改 | 指针接收者方法修改字段 |
| 相等性 | 基于ID而非值比较 | 实现Equals方法或直接比较ID |
| 生命周期 | 从创建到持久化再到删除 | 配合仓库模式管理 |
2.2 值对象(Value Object)
值对象通过属性值定义自身,没有唯一标识,且不可变。在Go中,值对象通常使用不可变结构体,通过构造函数创建,不暴露修改方法。
// 地址值对象
type Address struct {
street string // 小写字段,包外不可直接访问
city string
zipCode string
}
// 构造函数:确保值对象有效且不可变
func NewAddress(street, city, zipCode string) (Address, error) {
if street == "" || city == "" || zipCode == "" {
return Address{}, errors.New("all address fields are required")
}
return Address{street: street, city: city, zipCode: zipCode}, nil
}
// 值对象相等性:基于所有字段
func (a Address) Equals(other Address) bool {
return a.street == other.street &&
a.city == other.city &&
a.zipCode == other.zipCode
}
// 只读访问器
func (a Address) Street() string { return a.street }
func (a Address) City() string { return a.city }
func (a Address) ZipCode() string { return a.zipCode }
Go实践建议: 值对象字段使用小写字母,通过访问器方法暴露只读视图。这符合Go的封装哲学,同时保证不可变性。
2.3 聚合(Aggregate)
聚合是领域驱动设计中保证数据一致性的边界。一个聚合包含一个根实体和一组相关的实体/值对象,所有外部访问必须通过聚合根进行。
// 订单聚合根
type Order struct {
ID string
customerID string
items []OrderItem
shippingAddress Address
totalAmount Money
status OrderStatus
domainEvents []DomainEvent // 待发布的领域事件
}
// 通过聚合根添加订单项
func (o *Order) AddItem(productID string, quantity int, unitPrice Money) error {
if o.status != OrderStatusPending {
return errors.New("can only add items to pending orders")
}
item := NewOrderItem(o.generateItemID(), productID, quantity, unitPrice)
o.items = append(o.items, item)
o.recalculateTotal()
// 记录领域事件
o.addDomainEvent(OrderItemAddedEvent{
OrderID: o.ID,
ProductID: productID,
Quantity: quantity,
})
return nil
}
// 聚合内方法:重新计算总金额
func (o *Order) recalculateTotal() {
var total float64
for _, item := range o.items {
total += item.SubTotal().Amount()
}
o.totalAmount = NewMoney(total, "USD")
}
并发注意: 在Go中,聚合实例可能被多个goroutine访问。建议在聚合根的方法中使用sync.Mutex或通过通道串行化访问,避免并发修改导致的不一致。
2.4 领域事件(Domain Event)
领域事件记录聚合中发生的业务相关事实,用于解耦聚合间的通信。在事件溯源架构中,领域事件是状态变更的唯一来源。
// 领域事件接口
type DomainEvent interface {
EventID() string
OccurredAt() time.Time
EventType() string
}
// 具体领域事件:订单已创建
type OrderCreatedEvent struct {
EventIDVal string `json:"event_id"`
OccurredAtVal time.Time `json:"occurred_at"`
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
TotalAmount float64 `json:"total_amount"`
}
func (e OrderCreatedEvent) EventID() string { return e.EventIDVal }
func (e OrderCreatedEvent) OccurredAt() time.Time { return e.OccurredAtVal }
func (e OrderCreatedEvent) EventType() string { return "order.created" }
// 领域事件发布器接口
type EventPublisher interface {
Publish(event DomainEvent) error
}
// 聚合根中的事件收集与发布
func (o *Order) addDomainEvent(event DomainEvent) {
o.domainEvents = append(o.domainEvents, event)
}
func (o *Order) ReleaseEvents() []DomainEvent {
events := o.domainEvents
o.domainEvents = nil
return events
}
| 事件类型 | 触发场景 | 包含数据 |
|---|---|---|
| OrderCreated | 用户提交订单 | orderID, customerID, totalAmount |
| OrderItemAdded | 向订单添加商品 | orderID, productID, quantity |
| OrderShipped | 订单发货 | orderID, trackingNumber, shippedAt |
| PaymentCompleted | 支付成功 | orderID, paymentID, amount |
2.5 仓库模式(Repository Pattern)
仓库模式为聚合提供集合风格的访问接口,隐藏底层存储细节。在Go中,仓库通常定义为接口,支持依赖注入和测试替身。
// 仓库接口定义在领域层
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, orderID string) (*Order, error)
FindByCustomer(ctx context.Context, customerID string, page, size int) ([]*Order, error)
Delete(ctx context.Context, orderID string) error
}
// MySQL实现(基础设施层)
type MySQLOrderRepository struct {
db *sql.DB
}
func (r *MySQLOrderRepository) Save(ctx context.Context, order *Order) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// 保存聚合根
_, err = tx.ExecContext(ctx,
`INSERT INTO orders (id, customer_id, status, total_amount, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE status=?, total_amount=?, updated_at=?`,
order.ID, order.customerID, order.status, order.totalAmount.Amount(),
order.CreatedAt, order.UpdatedAt,
order.status, order.totalAmount.Amount(), order.UpdatedAt)
if err != nil {
return err
}
// 保存子实体
for _, item := range order.items {
_, err = tx.ExecContext(ctx,
`INSERT INTO order_items (id, order_id, product_id, quantity, unit_price)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE quantity=?, unit_price=?`,
item.ID, order.ID, item.ProductID, item.Quantity, item.UnitPrice.Amount(),
item.Quantity, item.UnitPrice.Amount())
if err != nil {
return err
}
}
return tx.Commit()
}
// 内存实现(测试用)
type InMemoryOrderRepository struct {
mu sync.RWMutex
orders map[string]*Order
}
func (r *InMemoryOrderRepository) Save(ctx context.Context, order *Order) error {
r.mu.Lock()
defer r.mu.Unlock()
r.orders[order.ID] = order
return nil
}
func (r *InMemoryOrderRepository) FindByID(ctx context.Context, orderID string) (*Order, error) {
r.mu.RLock()
defer r.mu.RUnlock()
order, ok := r.orders[orderID]
if !ok {
return nil, errors.New("order not found")
}
return order, nil
}
2.6 综合示例:订单创建流程
以下代码展示了实体、值对象、聚合、领域事件和仓库模式如何协同工作,完成一个完整的订单创建流程。
// 应用服务:订单服务
type OrderService struct {
repo OrderRepository
publisher EventPublisher
}
func (s *OrderService) CreateOrder(ctx context.Context, customerID string, items []CreateItemRequest, shippingAddr Address) (*Order, error) {
// 1. 创建聚合根
order := NewOrder(generateID(), customerID, shippingAddr)
// 2. 通过聚合根添加订单项
for _, item := range items {
unitPrice, _ := NewMoney(item.Price, "USD")
if err := order.AddItem(item.ProductID, item.Quantity, unitPrice); err != nil {
return nil, fmt.Errorf("failed to add item: %w", err)
}
}
// 3. 保存聚合(仓库保证原子性)
if err := s.repo.Save(ctx, order); err != nil {
return nil, fmt.Errorf("failed to save order: %w", err)
}
// 4. 发布领域事件
events := order.ReleaseEvents()
for _, event := range events {
if err := s.publisher.Publish(event); err != nil {
// 事件发布失败的处理策略:重试、记录日志、或使用发件箱模式
log.Printf("failed to publish event %s: %v", event.EventType(), err)
}
}
return order, nil
}
Go DDD 最佳实践:
- 实体和值对象放在
internal/domain包中,不依赖任何外部框架 - 仓库接口定义在领域层,实现放在
internal/infrastructure包 - 使用
wire或fx进行依赖注入,解耦各层 - 领域事件使用发件箱模式(Outbox Pattern)保证与聚合状态的一致性
- 聚合内使用
sync.Mutex保护并发访问,或通过通道串行化操作
2.7 本章小结
| 概念 | Go实现要点 | 与事件溯源的关系 |
|---|---|---|
| 实体 | 指针接收者方法、ID比较 | 事件溯源中实体的状态由事件流重建 |
| 值对象 | 不可变结构体、小写字段、访问器 | 作为事件载荷的一部分被序列化 |
| 聚合 | 聚合根封装、事务边界、事件收集 | 事件溯源中聚合是事件流的投影 |
| 领域事件 | 接口定义、事件收集与发布 | 事件溯源的核心存储单元 |
| 仓库 | 接口在领域层、实现可替换 | 事件溯源仓库基于事件存储实现 |