参考
CommandHandler
Handler 接口、Router 注册、只通过 Repository 访问聚合、转账等多聚合命令,以及重复类型注册会 panic。
CommandHandler
包:command
Handler 把 Command 翻译成对聚合的调用,是 CommandService 与领域模型之间的粘合层。
接口
type Handler interface {
Handles() []string
Handle(ctx context.Context, cmd Command) error
}
Handles() 列出该处理器接受的命令类型。Handle 收到的命令已经过中间件、过滤器和校验。
处理多个命令的 Handler
type Handler struct {
Repo *aggregate.Repository
}
func (h *Handler) Handles() []string {
return []string{CmdOpenAccount, CmdDeposit, CmdWithdraw, CmdTransfer}
}
func (h *Handler) Handle(ctx context.Context, cmd command.Command) error {
switch c := cmd.(type) {
case *OpenAccount:
acc := NewAccount(c.AccountID)
if tid := c.Meta().TenantID; tid != uuid.Nil {
acc.SetPartitionKey(tenancy.PartitionKey(tid))
}
if err := acc.OpenAccount(c.Owner, c.Currency, c.Opening); err != nil {
return err
}
return h.Repo.Add(ctx, acc)
case *Deposit:
agg, err := h.Repo.Load(ctx, c.AccountID, AggregateAccount)
if err != nil {
return err
}
return agg.(*Account).Deposit(c.Amount)
default:
return fmt.Errorf("bank: unhandled command %T", cmd)
}
}
可以每个聚合类型一个 Handler,也可以每个命令一个——只要每个命令类型恰好对应一个 Handler。
规则
- 只用
Repo.Add与Repo.Load。 不要在 Handler 里直接调EventStore.Append;Repository 在提交时追加。 - 新聚合用
Add,已有聚合用Load。 两者都要求 context 上有 UnitOfWork,由Execute提供。 - 不要自己提交。 Handler 返回后,
CommandService会调Repository.Commit。 - 返回错误即中止。 任何错误都会回滚整次
Execute事务。 - 保持 Handler 精简。 业务规则放在聚合方法里,这样
testkit不用数据库也能测到。
多聚合命令
一条命令可以碰多个聚合。在同一次 Handle 里加载它们,就共享同一个 UnitOfWork 与事务:
case *Transfer:
from, err := h.Repo.Load(ctx, c.FromAccountID, AggregateAccount)
if err != nil {
return err
}
to, err := h.Repo.Load(ctx, c.ToAccountID, AggregateAccount)
if err != nil {
return err
}
fromAcc, toAcc := from.(*Account), to.(*Account)
if fromAcc.Currency != toAcc.Currency {
return fmt.Errorf("currency mismatch between accounts")
}
if err := fromAcc.DebitTransfer(c.Amount, c.ToAccountID.String(), c.TransferID); err != nil {
return err
}
return toAcc.CreditTransfer(c.Amount, c.FromAccountID.String(), c.TransferID)
提交时事件按加载顺序发布。每个聚合有各自的 ExpectedVersion,任一事件流冲突都会让整条命令失败。见 Repository。
注册
app.RegisterHandler(&Handler{Repo: app.Repo})
这会调用 Router.Register,把 Handles() 里的每个类型映射到该 Handler。两个 Handler 声明同一命令类型时 Router.Register 会 panic,错误在启动时就能发现。分发没有 Handler 的类型会返回:command: no handler for "..."。
Handler 与 Workflow
Handler 应当是「命令 + 它加载的聚合」的纯函数。不能回滚的副作用(例如发邮件)放到 Workflow 的 AfterCommit 钩子或 Outbox。如果 Handler 必须调外部系统,只做获取外部 id 这类短调用;余额事实仍交给 SyncInTxn(在写事务里同步更新读模型)Projector——把事件写成查询用读模型(例如余额表)的组件。
授权
聚合级检查(例如账户归属)写在聚合方法里,只用聚合状态与命令携带的声明。命令级权限检查放在 Handler 之前运行的 Authorizer 过滤器里。
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。