跳到正文

参考

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 不一致,以源码为准。