4. CQRS 命令模型设计:Command 对象定义、命令总线模式与处理器注册分发

命令模型是 CQRS 架构的写入端核心。与查询模型不同,命令模型表达用户的意图,而非数据查询。本章深入 Command 对象定义、命令总线(Command Bus)模式以及命令处理器(Command Handler)的注册与分发机制,并结合 Go 语言的并发特性,构建高性能、类型安全的命令处理管道。

4.1 Command 对象定义:意图的载体

Command 对象是一个不可变的数据传输对象(DTO),它封装了执行某个操作所需的所有信息。在 Go 中,我们通常使用结构体来定义 Command,并遵循以下原则:

  • 命名使用祈使语气:如 CreateOrderCommand、UpdateInventoryCommand。
  • 字段为值类型或简单类型:避免包含业务逻辑或复杂引用。
  • 支持验证:在命令进入总线前进行基本校验。
设计要点:Command 不应包含聚合根引用或仓储实例。它仅仅是数据的载体,由命令处理器负责将数据映射到领域模型。

以下是一个典型的 Command 定义示例:

// CreateOrderCommand 创建订单命令
type CreateOrderCommand struct {
    OrderID   string    `json:"order_id" validate:"required,uuid"`
    UserID    string    `json:"user_id" validate:"required,uuid"`
    Items     []ItemDTO `json:"items" validate:"required,min=1,dive"`
    CreatedAt time.Time `json:"created_at"`
}

// ItemDTO 订单项数据传输对象
type ItemDTO struct {
    ProductID string `json:"product_id" validate:"required,uuid"`
    Quantity  int    `json:"quantity" validate:"required,min=1"`
    UnitPrice int64  `json:"unit_price" validate:"required,gt=0"`
}

// Validate 执行命令基本校验
func (cmd *CreateOrderCommand) Validate() error {
    // 使用 validator 库或自定义校验逻辑
    if len(cmd.Items) == 0 {
        return errors.New("order must contain at least one item")
    }
    return nil
}
属性 说明 示例
不可变性 命令一旦创建,字段不应被修改 使用值接收者,不暴露 Setter
序列化友好 支持 JSON/Protobuf 序列化 添加 json 标签
自验证 提供 Validate() 方法 在总线层调用
幂等键 可选字段,用于去重 IdempotencyKey string

4.2 命令总线(Command Bus)模式

命令总线是 CQRS 架构中的核心中介者。它接收 Command 对象,并将其路由到对应的命令处理器。总线模式实现了发送者与处理者的解耦,使得添加新命令无需修改现有代码。

在 Go 中,命令总线通常是一个接口,定义如下:

// CommandBus 命令总线接口
type CommandBus interface {
    // Dispatch 同步分发命令,返回结果或错误
    Dispatch(ctx context.Context, cmd interface{}) (interface{}, error)
    // DispatchAsync 异步分发命令,返回 Future 或 channel
    DispatchAsync(ctx context.Context, cmd interface{}) (<-chan interface{}, <-chan error)
    // Register 注册命令类型对应的处理器
    Register(cmdType reflect.Type, handler CommandHandler) error
}

// CommandHandler 命令处理器接口
type CommandHandler interface {
    Handle(ctx context.Context, cmd interface{}) (interface{}, error)
}
并发提示:在 Go 中,命令总线内部通常使用 sync.RWMutex 保护处理器注册表,并使用 goroutine 池处理异步命令,以充分利用多核性能。

命令总线的核心工作流程如下:

  1. 客户端创建 Command 对象并调用 Dispatch。
  2. 总线根据 Command 的类型(reflect.Type)查找已注册的处理器。
  3. 总线执行中间件管道(如日志、事务、验证)。
  4. 最终调用 CommandHandler.Handle 方法。
  5. 处理器返回结果或错误,总线将其返回给调用方。
命令总线核心流程 客户端 Command 对象 命令总线 中间件管道 日志 验证 事务 命令处理器 聚合根 返回结果

4.3 命令处理器注册与分发

命令处理器的注册与分发是命令总线的核心实现。在 Go 中,我们利用 reflect 包实现类型到处理器的映射,同时保持类型安全。

4.3.1 注册机制

注册阶段将 Command 类型与对应的处理器绑定。通常采用以下两种方式:

方式 实现 优点 缺点
反射注册 使用 reflect.TypeOf 作为 key 通用性强,无需手动指定类型名称 运行时开销,类型安全需额外检查
泛型注册(Go 1.18+) 使用泛型约束 CommandType 编译期类型安全,性能更优 代码复杂度增加,版本兼容性要求

以下是基于反射的注册实现示例:

// InMemoryCommandBus 内存命令总线实现
type InMemoryCommandBus struct {
    mu       sync.RWMutex
    handlers map[reflect.Type]CommandHandler
    middleware []MiddlewareFunc
}

// Register 注册命令处理器
func (bus *InMemoryCommandBus) Register(cmdType reflect.Type, handler CommandHandler) error {
    bus.mu.Lock()
    defer bus.mu.Unlock()
    
    if _, exists := bus.handlers[cmdType]; exists {
        return fmt.Errorf("handler already registered for command type: %v", cmdType)
    }
    bus.handlers[cmdType] = handler
    return nil
}

// Dispatch 同步分发命令
func (bus *InMemoryCommandBus) Dispatch(ctx context.Context, cmd interface{}) (interface{}, error) {
    cmdType := reflect.TypeOf(cmd)
    
    bus.mu.RLock()
    handler, exists := bus.handlers[cmdType]
    bus.mu.RUnlock()
    
    if !exists {
        return nil, fmt.Errorf("no handler registered for command type: %v", cmdType)
    }
    
    // 执行中间件链
    finalHandler := applyMiddleware(handler, bus.middleware)
    return finalHandler.Handle(ctx, cmd)
}

4.3.2 分发策略

命令分发支持同步和异步两种模式,以适应不同的业务场景:

  • 同步分发:调用方阻塞等待处理结果,适用于需要立即确认的命令(如创建订单)。
  • 异步分发:命令进入队列后立即返回,适用于后台处理或最终一致性场景(如发送邮件)。
并发安全:异步分发时,命令处理器可能被多个 goroutine 并发调用。确保处理器内部对共享资源的访问是线程安全的,或为每个聚合根实例使用独立的 Actor 模型。
命令处理器注册与分发架构 注册阶段 处理器注册表 CreateOrder → HandlerA UpdateStock → HandlerB DeleteItem → HandlerC 分发阶段 命令队列 (缓冲/持久化) 命令分发器 (类型查找 + 中间件) 执行阶段 HandlerA (goroutine) HandlerB (goroutine) HandlerC (goroutine)

4.4 中间件管道与横切关注点

命令总线支持中间件模式,用于处理横切关注点。每个中间件包装下一个处理器,形成责任链:

// MiddlewareFunc 中间件函数类型
type MiddlewareFunc func(CommandHandler) CommandHandler

// LoggingMiddleware 日志中间件
func LoggingMiddleware(logger *log.Logger) MiddlewareFunc {
    return func(next CommandHandler) CommandHandler {
        return &loggingHandler{next: next, logger: logger}
    }
}

type loggingHandler struct {
    next   CommandHandler
    logger *log.Logger
}

func (h *loggingHandler) Handle(ctx context.Context, cmd interface{}) (interface{}, error) {
    cmdType := reflect.TypeOf(cmd).String()
    h.logger.Printf("handling command: %s", cmdType)
    start := time.Now()
    
    result, err := h.next.Handle(ctx, cmd)
    
    h.logger.Printf("command %s completed in %v, err=%v", cmdType, time.Since(start), err)
    return result, err
}

// applyMiddleware 应用中间件链
func applyMiddleware(handler CommandHandler, middlewares []MiddlewareFunc) CommandHandler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}
最佳实践:将事务管理、验证、审计日志、指标收集等横切关注点实现为中间件,保持命令处理器专注于领域逻辑。

4.5 总结

本章详细介绍了 CQRS 命令模型的核心设计:

  • Command 对象作为意图的不可变载体,支持自验证和序列化。
  • 命令总线实现发送者与处理者的解耦,支持同步/异步分发。
  • 处理器注册利用反射或泛型实现类型到处理器的映射。
  • 中间件管道优雅地处理横切关注点,提升代码复用性。