[Из песочницы] Можно ли использовать CQRS паттерн в GO?

habr.png

Паттерн (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.

© Habrahabr.ru