[Из песочницы] Можно ли использовать CQRS паттерн в GO?
Паттерн (CQRS — Command and Query Responsibility Segregation) разделяющей в своей основе команды по чтению данных от команд по их модификации или добавлению по средствам различных интерфейсов. Это позволяет достичь максимальную производительность, масштабируемость и безопасность, а также позволяет увеличить гибкость системы к модификациям с течением времени и снизить количество ошибок при усложнении логики системы, причиной которых обычно является обработка данных на доменном уровне.
Этот паттерн не новый и относится еще к языку Eiffel. Но благодаря усилиями Greg Young-а и Martin Fowler-а получил реинкарнацию, особенно в .NET мире.
По своему личному опыту, могу сказать, что паттерн очень удобный, довольно легко позволяет разделить бизнес логику, очень хорошо поддерживаемый, легко «усваиваемый» новичками, а также хорошо позволяющий изолировать ошибки. О нем много написано информации на хабре и нет смысл еще раз перепечатывать это все в этой статье. Тут я решил полностью сфокусироваться на портировании этого паттерна в Go.
Итак начнем…
Сразу хотело бы сказать, что CQRS не обязательно подразумевает Event Sourcing, но часто используется в связке CQRS +DDD + Event Sourcing из-за простоты реализации последнего. Я пробовал проекты вместе с Event Sourcing и без в него, и в обоих случаях патерн CQRS хорошо ложился на бизнес логику, но стоит заметить, что использованиe Event Sourcing приносит свои преимущества при реализации денормализованной базы данных, скорости сохранения и чтения данных, но при этому усложняет понимание данных, миграцию и формирование отчетности.
Проще говоря для Event Sourcing необходимо использовать CQRS, но не наоборот.
Поэтому я не буду касаться Event Sourcing, чтобы не усложнять статью.
И так для обработки Command нам понадобится Bus. Определим ее базовый интерфейс
type EventBus interface {
Subscribe(handler handlers.Handler) error
Publish(eventName string, args ...interface{})
Unsubscribe(eventName string) error
}
Также нам нужно где-то хранить логику обработки команд. Для этого определим интерфейс обработчиков — Handler
type Handler interface {
Event() string
Execute(... interface{}) error
OnSubscribe()
OnUnsubscribe()
}
Во многих контрактных языках обычно добавляют еще интерфейс для Command, но в силу реализации интерфейсов в Go это не имеет смысла, и команда заменятся событием — Event, которое может быть обработано множеством Handler-ов.
Если вы уже заметили, то в метод Publish передается набор параметров — Publish (eventName string, args …interface{}), а не определенный типа. В результате чего вы можете передать в метод любой тип или набор типов.
Полная реализация EventBus будет выглядеть так:
type eventBus struct {
mtx sync.RWMutex
handlers map[string][] handlers.Handler
}
// Execute appropriate handlers
func (b *eventBus) Publish(eventName string, args ...interface{}) {
b.mtx.RLock()
defer b.mtx.RUnlock()
if hs, ok := b.handlers[eventName]; ok {
rArgs := createArgs(args)
for _, h := range hs {
// In case of Closure "h" variable will be reassigned before ever executed by goroutine.
// Because if this you need to save value into variable and use this variable in closure.
h_in_goroutine := h
go func() {
//Handle Panic in Handler.Execute.
defer func() {
if err := recover(); err != nil {
log.Printf("Panic in EventBus.Publish: %s", err)
}
}()
h_in_goroutine.Execute(rArgs)
}()
}
}
}
// Subscribe Handler
func (b *eventBus) Subscribe(h handlers.Handler) error {
b.mtx.Lock()
//Handle Panic on adding new handler
defer func() {
b.mtx.Unlock()
if err := recover(); err != nil {
log.Printf("Panic in EventBus.Subscribe: %s", err)
}
}()
if h == nil {
return errors.New("Handler can not be nil.")
}
if len(h.Event()) == 0 {
return errors.New("Handlers with empty Event are not allowed.")
}
h.OnSubscribe()
b.handlers[h.Event()] = append(b.handlers[h.Event()], h)
return nil
}
// Unsubscribe Handler
func (b *eventBus) Unsubscribe(eventName string) error {
b.mtx.Lock()
//Handle Panic on adding new handler
defer func() {
b.mtx.Unlock()
if err := recover(); err != nil {
log.Printf("Panic in EventBus.Unsubscribe: %s", err)
}
}()
if _, ok := b.handlers[eventName]; ok {
for i, h := range b.handlers[eventName] {
if h != nil {
h.OnUnsubscribe()
b.handlers[eventName] = append(b.handlers[eventName][:i], b.handlers[eventName][i+1:]...)
}
}
return nil
}
return fmt.Errorf("event are not %s exist", eventName)
}
func createArgs(args []interface{}) []reflect.Value {
reflectedArgs := make([]reflect.Value, 0)
for _, arg := range args {
reflectedArgs = append(reflectedArgs, reflect.ValueOf(arg))
}
return reflectedArgs
}
// New creates new EventBus
func New() EventBus {
return &eventBus{
handlers: make(map[string][] handlers.Handler),
}
}
Внутри метода Publish вызывается метод обработчика событие обернутый в goroutine с обработкой panic, на случай непредвиденной ситуации.
Как видите реализация очень простая, гораздо проще, чем это можно реализовать на .NET или Java.
Полный код с примерами вы может скачать тут: github.