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

[Из песочницы] Можно ли использовать 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.


x

Ещё Hi-Tech Интересное!

[Из песочницы] От var b до собеседования

Вы почти закончили универ или колледж? Вас пригласили на собеседования, но вы идете туда без подготовки? У вас нет образования (высшего), но хотите работать программистом или в сфере IT? Речь пойдёт по большей степени о поиске работы, я буду говорить ...

OpenSceneGraph: Основы работы с геометрией сцены

OpenGL, являющийся бэкэндом для OpenSceneGraph, использует геометрические примитивы (такие как точки, линии, треугольники и полигональные грани) для построения всех объектов трехмерного мира. Эти данные хранятся в специальных массивах. Эти примитивы задаются данными об их вершинах, в которые входят координаты вершин, ...