visit
Mediator is a behavioural design pattern that reduces dependencies and encapsulates the communication between components inside a component called the Mediator. It centralises control, simplifies protocols, decouples different parts of the system, and abstracts how components interact.
Event Aggregator is mix of another behavioural design pattern called Observer and Mediator itself. In its simplest the component called the Event Aggregator is a single source of events for many components. The consumers are registered to the Event Aggregator and on the other end the producers publish their events using the Event Aggregator.
mob is a simple, open-source, generic-based Mediator / Event Aggregator library. It solves complex dependency management by introducing a single communication point. mob handles both request-response and event-based communication.
type RequestHandler[T any, U any] interface {
Handle(context.Context, T) (U, error)
}
mob permits to use any type for both requests and responses. To make the RequestHandler
interface easier to implement, mob allows the use of ordinary functions as request handlers.
type EventHandler[T any] interface {
Handle(context.Context, T) error
}
go get github.com/erni27/mob
Create your first request handler
type UserDTO struct {
// User data.
}
type UserQuery struct {
// Necessary dependencies.
}
func (q UserQuery) Get(context.Context, int) (UserDTO, error) {
// Your code.
}
Remember, your handler implementation doesn’t have to satisfy the RequestHandler
interface directly, you can use a struct method or an ordinary function as a request handler as long as it has the signature func(context.Context, any) (any, error)
.
Register your handler. Since we use the struct’s method as a request handler, we need to convert it to the RequestHandlerFunc
. It’s possible to register only one handler per request / response type.
// Somewhere in initialisation code.
uq := query.NewUserQuery(/* parameters */)
err := mob.RegisterRequestHandler[int, query.UserDTO](
mob.RequestHandlerFunc[int, query.UserDTO](uq.Get),
)
func GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
}
res, err := mob.Send[int, UserDTO](req.Context(), id)
if err != nil {
// Err handling.
}
w.Header().Set("content-type", "application/json")
if err := json.NewEncoder(w).Encode(res); err != nil {
// Err handling.
}
}
Create your first event handler
type OrderCreated struct {
// Event data.
}
type DeliveryService struct {
// Necessary dependencies.
}
func (s DeliveryService) PrepareDelivery(ctx context.Context, event event.OrderCreated) error {
// Your logic.
}
Register your handler. Again, we want to use a struct’s method as an event handler so the conversion to the EventHandlerFunc
is necessary. Unlike request handlers, multiple event handlers can be registered per one event type. All of them are executed concurrently if an event occurs.
// Somewhere in initialisation code.
ds := delivery.NewService(/* parameters */)
err := mob.RegisterEventHandler[event.OrderCreated](
mob.EventHandlerFunc[event.OrderCreated](ds.PrepareDelivery),
)
type OrderService struct {
// Necessary dependencies.
}
func (s OrderService) CreateOrder(ctx context.Context, cmd command.CreateOrder) error {
// Your logic. #1
err := mob.Notify[event.OrderCreated](ctx, event.OrderCreated{ /* init event */ })
// Your logic. #2
}
Mediator and EventAggregator patterns are powerful concepts that make complex dependency management easy. Tools built on top of them are convenient when applying more advanced patterns like CQRS. Although useful, they should be used carefully. Apply them only where needed. If a centralised point of communication within your domain obscures it and makes it harder to understand, you should consider sticking to the direct, more traditional way of communication.