CQRS
CQRS 的意思是“命令 - 查问责任隔离”。咱们拆散了命令 (写申请) 和查问 (读申请) 之间的责任。写申请和读申请由不同的对象解决。
就是这样。咱们能够进一步宰割数据存储,应用独自的读写存储。一旦产生这种状况,可能会有许多读取存储,这些存储针对解决不同类型的查问或逾越多个边界上下文进行了优化。尽管常常探讨与 CQRS 相干的独自读写存储,但这并不是 CQRS 自身。CQRS 只是命令和查问的第一局部。
术语
Command
该命令是一个简略的数据结构,示意执行某些操作的申请。
Command Bus
残缺源码:
- github.com/ThreeDotsLabs/watermill/components/cqrs/command_bus.go
// ...
// CommandBus 将命令 (commands) 传输到命令处理程序(command handlers)。type CommandBus struct {// ...
Command Processor
残缺源码:
- github.com/ThreeDotsLabs/watermill/components/cqrs/command_processor.go
// ...
// CommandProcessor 决定哪个 CommandHandler 应该解决这个命令
received from the command bus.
type CommandProcessor struct {// ...
Command Handler
残缺源码:
- github.com/ThreeDotsLabs/watermill/components/cqrs/command_processor.go
// ...
// CommandHandler 接管由 NewCommand 定义的命令,并应用 Handle 办法解决它。// 如果应用 DDD, CommandHandler 能够批改并长久化聚合。//
// 与 EvenHandler 相同,每个命令必须只有一个 CommandHandler。//
// 在解决音讯期间应用 CommandHandler 的一个实例。// 当同时发送多个命令时,Handle 办法能够同时执行屡次。// 因而,Handle 办法必须是线程平安的!type CommandHandler interface {// ...
Event
该事件示意曾经产生的事件。事件是不可变的。
Event Bus
残缺源码:
- github.com/ThreeDotsLabs/watermill/components/cqrs/event_bus.go
// ...
// EventBus 将事件传输到事件处理程序。type EventBus struct {// ...
Event Processor
残缺源码:
- github.com/ThreeDotsLabs/watermill/components/cqrs/event_processor.go
// ...
// EventProcessor 确定哪个 EventHandler 应该解决从事件总线接管到的事件。type EventProcessor struct {// ...
Event Handler
残缺源码:
- github.com/ThreeDotsLabs/watermill/components/cqrs/event_processor.go
// ...
// EventHandler 接管由 NewEvent 定义的事件,并应用其 Handle 办法对其进行解决。// 如果应用 DDD,CommandHandler 能够批改并保留聚合。// 它还能够调用流程管理器、saga 或仅仅构建一个读取模型。// 与 CommandHandler 相比,每个 Event 能够具备多个 EventHandler。//
// 在解决音讯时应用 EventHandler 的一个实例。// 当同时传递多个事件时,Handle 办法能够同时执行屡次。// 因而,Handle 办法必须是线程平安的!type EventHandler interface {// ...
CQRS Facade
残缺源码:
- github.com/ThreeDotsLabs/watermill/components/cqrs/cqrs.go
// ...
// Facade 是用于创立 Command 和 Event buses 及 processors 的 facade。// 创立它是为了在以规范形式应用 CQRS 时防止应用 boilerplate。// 您还能够手动创立 buses 和 processors,并从 NewFacade 中取得灵感。type Facade struct {// ...
Command and Event Marshaler
残缺源码:
- github.com/ThreeDotsLabs/watermill/components/cqrs/marshaler.go
// ...
// CommandEventMarshaler 将命令和事件 marshal 给 Watermill 的音讯,反之亦然。// 该命令的有效载荷须要 marshal 至 []bytes。type CommandEventMarshaler interface {
// Marshal marshal 命令或事件给 Watermill 的音讯。Marshal(v interface{}) (*message.Message, error)
// Unmarshal Unmarshal watermill 的信息给 v Command 或 Event。Unmarshal(msg *message.Message, v interface{}) (err error)
// Name 返回命令或事件的名称。// Name 用于确定接管到的命令或事件是咱们想要解决的事件。Name(v interface{}) string
// NameFromMessage 从 Watermill 的音讯 (由 Marshal 生成) 中返回命令或事件的名称。//
//
// 当咱们将 Command 或 Event marshal 到 Watermill 的音讯中时,// 咱们应该应用 NameFromMessage 而不是 Name 来防止不必要的 unmarshaling。NameFromMessage(msg *message.Message) string
}
// ...
用法
Example domain(畛域模型示例)
作为示例,咱们将应用一个简略的 domain,它负责解决酒店的房间预订。
咱们将应用 Event Storming 表示法来展现这个 domain 的模型。
Legend:
- blue(蓝色)便当贴是命令
- orange(橙色)便当贴是事件
- green(绿色)便当贴是读取模型,从事件异步生成
- violet(紫色)便当贴是策略,由事件触发并产生命令
- pink(粉色)便当贴是热点(hot-spots);咱们标记常常产生问题的中央
domain(畛域模型)很简略:
- 客人能够预订房间(book a room)。
-
每当预订房间时,咱们都会为客人订购啤酒(Whenever a room is booked, we order a beer)(因为咱们爱客人)。
- 咱们晓得有时候啤酒不够(not enough beers)。
- 咱们依据预订生成财务报告(financial report)。
Sending a command(发送命令)
首先,咱们须要模仿访客的动作。
残缺源码:
- github.com/ThreeDotsLabs/watermill/_examples/basic/5-cqrs-protobuf/main.go
// ...
bookRoomCmd := &BookRoom{RoomId: fmt.Sprintf("%d", i),
GuestName: "John",
StartDate: startDate,
EndDate: endDate,
}
if err := commandBus.Send(context.Background(), bookRoomCmd); err != nil {panic(err)
}
// ...
Command handler
BookRoomHandler 将解决咱们的命令。
残缺源码:
- github.com/ThreeDotsLabs/watermill/_examples/basic/5-cqrs-protobuf/main.go
// ...
// BookRoomHandler 是一个命令处理程序,它解决 BookRoom 命令并收回 RoomBooked。//
// 在 CQRS 中,一个命令只能由一个处理程序解决。// 将具备此命令的另一个处理程序增加到命令处理器时,将返回谬误。type BookRoomHandler struct {eventBus *cqrs.EventBus}
func (b BookRoomHandler) HandlerName() string {return "BookRoomHandler"}
// NewCommand 返回该 handle 应该解决的命令类型。它必须是一个指针。func (b BookRoomHandler) NewCommand() interface{} {return &BookRoom{}
}
func (b BookRoomHandler) Handle(ctx context.Context, c interface{}) error {
// c 始终是 `NewCommand` 返回的类型,因而强制转换始终是平安的
cmd := c.(*BookRoom)
// 一些随机的价格,在生产中你可能会用更理智的形式计算
price := (rand.Int63n(40) + 1) * 10
log.Printf(
"Booked %s for %s from %s to %s",
cmd.RoomId,
cmd.GuestName,
time.Unix(cmd.StartDate.Seconds, int64(cmd.StartDate.Nanos)),
time.Unix(cmd.EndDate.Seconds, int64(cmd.EndDate.Nanos)),
)
// RoomBooked 将由 OrderBeerOnRoomBooked 事件处理程序解决,