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 池处理异步命令,以充分利用多核性能。
命令总线的核心工作流程如下:
- 客户端创建 Command 对象并调用
Dispatch。 - 总线根据 Command 的类型(
reflect.Type)查找已注册的处理器。 - 总线执行中间件管道(如日志、事务、验证)。
- 最终调用
CommandHandler.Handle方法。 - 处理器返回结果或错误,总线将其返回给调用方。
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 模型。
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 对象作为意图的不可变载体,支持自验证和序列化。
- 命令总线实现发送者与处理者的解耦,支持同步/异步分发。
- 处理器注册利用反射或泛型实现类型到处理器的映射。
- 中间件管道优雅地处理横切关注点,提升代码复用性。