mob

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2022 License: MIT Imports: 5 Imported by: 0

README

mob

GitHub Workflow Status Go Report Card Go Version GoDoc Coverage Status Mentioned in Awesome Go

mob is a generic-based, simple mediator / observer (event aggregator) library.

It supports in-process requests / events processing.

Motivation

I was a bit tired of managing dependencies between handlers. Reusing them became the existential issue. That's how mob has been created. It solves complex dependency management by introducing a single communication point. The mediator part encapsulates request-response communication while the observer one acts as a facade focused on observer relationships. mob is conceptually similiar to Event aggregator described by Martin Fowler.

mob supports two types of handlers - request handlers and event handlers.

Request handlers

A request handler responses to a particular request.

Request handlers can be registered through the RegisterRequestHandler method.

type DummyHandler struct{}

func (DummyHandler) Handle(ctx context.Context, req DummyRequest) (DummyResponse, error) {
    // Logic.
}

...

func main() {
    handler := DummyHandler{}
    if err := mob.RegisterRequestHandler[DummyRequest, DummyResponse](handler); err != nil {
        log.Fatalf("register handler: %v", err)
    }
}

A handler to register must satisfy the RequestHandler interface. Both request and response can have arbitrary data types.

Only one handler for a particular request-response pair can be registered. To avoid handlers conflicts use type alias declarations.

To send a request and get a response simply call the Send method.

// Somewhere in your code.
response, err := mob.Send[DummyRequest, DummyResponse](ctx, req)

If a handler does not exist for a given request - response pair - ErrHandlerNotFound is returned.

Interceptors

The processing can get complex, especially when building large, enterprise systems. It's necessary to add many cross-cutting concerns like logging, monitoring, validations or security. To make it simple, mob supports Interceptors. Interceptors allow to intercept an invocation of Send method so they offer a way to enrich the request-response processing pipeline (basically apply decorators).

Interceptors can be added to mob by calling AddInterceptor method.

mob.AddInterceptor(LoggingInterceptor)

Interceptors are invoked in order they're added to the chain.

For more information on how to create and use Interceptors, see the example.

Event handlers

An event handler executes some logic in response to a dispatched event.

Event handlers can be registered through the RegisterEventHandler method.

type DummyHandler struct{}

func (DummyHandler) Handle(ctx context.Context, req DummyRequest) error {
    // Logic.
}

...

func main() {
    handler := DummyHandler{}
    if err := mob.RegisterEventHandler[DummyRequest](handler); err != nil {
        log.Fatalf("register handler: %v", err)
    }
}

A handler to register must satisfy the EventHandler interface. A request can have an arbitrary data type.

Event handlers are almost identical to the request ones. There are a few subtle differences though. An event handler does not return a response, only an error in case of failure. Unlike request ones, multiple handlers for a given request type can be registered. Be careful, mob doesn't check if a concrete handler is registered multiple times. Type alias declarations solves handler conflicts.

To notify all registered handlers about a certain event call the Notify method.

// Somewhere in your code.
err := mob.Notify(ctx, event)

mob executes all registered handlers concurrently. If at least one of them fails, an aggregate error containing all errors is returned.

Named handlers

It's recommended to register a handler with a meaningful name. WithName is used to return an Option that associates a given name with a handler.

err := mob.RegisterEventHandler[LogEvent](LogEventHandler{}, mob.WithName("LogEventHandler"));

It helps debugging potential issues. Extremely useful when multiple event handlers are registered to the specific subject and there is a need to communicate which handler fails. mob prefixes all errors by a handler's name if configured.

Register ordinary functions as handlers

mob exports both RequestHandlerFunc and EventHandlerFunc that act as adapters to allow the use of ordinary functions (and structs' methods) as request and event handlers.

var hf mob.RequestHandlerFunc[DummyRequest, DummyResponse] = func(ctx context.Context, req DummyRequest) (DummyResponse, error) {
    // Your logic goes here.
}
err := mob.RegisterRequestHandler[DummyRequest, DummyResponse](hf)

Concurrency

mob is a concurrent-safe library for multiple requests and events processing. But you shouldn't mix handlers' registration with requests or events processing. mob assumes that clients register their handlers during the initialization process and after first request or event is processed - no handler is registered.

Use cases

There are many use cases for mob. Everytime when there is a burden of dependency management, mob can become a useful friend.

There are two cases where I find mob extremely useful.

The first one is to slim the application layer API handlers. mob centralizes control so there is no need to use DI. It makes the components more portable.

The following example shows one of the most popular kind of the application layers handlers - HTTP handlers.

Classic way

func GetUserHandler(u UserGetter) http.HandlerFunc {
    return func(rw http.ResponseWriter, req *http.Request) {
        var dureq DummyUserRequest
        _ = json.NewDecoder(req.Body).Decode(&dureq)
        res, _ := u.Get(req.Context(), dureq)
        rw.Header().Set("content-type", "application/json")
        rw.WriteHeader(http.StatusOK)
        _ = json.NewEncoder(rw).Encode(res)
    }
}

mob way

func GetUser(rw http.ResponseWriter, req *http.Request) {
    var dureq DummyUserRequest
    _ = json.NewDecoder(req.Body).Decode(&dureq)
    res, _ := mob.Send[DummyUserRequest, DummyUserResponse](req.Context(), dureq)
    rw.Header().Set("content-type", "application/json")
    rw.WriteHeader(http.StatusOK)
    _ = json.NewEncoder(rw).Encode(res)
}

mob is a convenient tool for applying CQS and CQRS.

mob also makes it easier to take advantage of any kind of in-process, event-based communication. A domain event processing is a great example.

Classic way

func (s *UserService) UpdateEmail(ctx context.Context, id string, email string) error {
    u, _ := s.Repository.GetUser(ctx, id)
    u.Email = email
    _ = s.Repository.UpdateUser(ctx, u)
    _ = s.ContactBookService.RefreshContactBook(ctx)
    _ = s.NewsletterService.RefreshNewsletterContactInformation(ctx)
    // Do more side-effect actions in response to the email changed event.
    return nil
}

mob way

func (s *UserService) UpdateEmail(ctx context.Context, id string, email string) error {
    u, _ := s.Repository.GetUser(ctx, id)
    u.Email = email
    _ = s.Repository.UpdateUser(ctx, u)
    _ = mob.Notify(ctx, EmailChanged{UserID: id, Email: email})
    return nil
}

For more information on how to use the global mob instance, see the example.

Multiple mobs

All previous examples correspond to the global mob (singleton based approach).

Although, mob itself acts as a global handlers registry. It is possible to configure as many as mobs (so multiple mob instances) as you want. Each mob instance acts as a separate handlers registry. mob package uses slightly different API to support multiple mob instances (mostly due to currently supported generic model which doesn't allow method type parameters).

To initialise a new, standalone mob instance use the New method.

m := mob.New()

RegisterRequestHandlerTo is used to register a request handler to the standalone mob instance. Pass the mob instance as a first function parameter followed by a handler to register.

err := mob.RegisterRequestHandlerTo[EchoRequest, EchoResponse](m, EchoRequestHandler{})

Because current Go design doesn't support the method having type parameters, mob uses facilitators to get advantage of mob's generic behaviour. Creating a RequestSender tied to a standalone mob instance must precede sending a request through the Send method.

res, err := mob.NewRequestSender[EchoRequest, EchoResponse](m).Send(ctx, "Hello world!")

Working with event handlers is similiar.

To register an event handler call the RegisterEventHandlerTo.

err := mob.RegisterEventHandlerTo[LogEvent](m, LogEventHandler{});

In order to notify an occurance of an event create an EventNotifier tied to a standalone mob instance and then call the Notify method.

err := mob.NewEventNotifier[LogEvent](m).Notify(ctx, "Hello world!")

To add an Interceptor to a standalone mob instance call the AddInterceptorTo method.

mob.AddInterceptorTo(m, LoggingInterceptor)

mob package keep track only of the global mob instance. It means that users are responsible for keeping track of the multiple, standalone mob instances.

For more information on how to create and use a standalone mob instance, see the example.

Conclusion

Although mob can be exteremely useful. It has some drawbacks. It makes an explicit communication implicit - in many cases a direct communication is much better than an indirect one. Especially when it obscures your domain.

Documentation

Overview

Package mob is a simple mediator / observer library. It supports in-process requests / events processing.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrHandlerNotFound indicates that a requested handler is not registered.
	ErrHandlerNotFound = errors.New("mob: handler not found")
	// ErrInvalidHandler indicates that a given handler is not valid.
	ErrInvalidHandler = errors.New("mob: invalid handler")
	// ErrDuplicateHandler indicates that a handler for a given request / response pair is already registered.
	// It applies only to request handlers.
	ErrDuplicateHandler = errors.New("mob: duplicate handler")
	// ErrUnmarshal indicates that a request or a response type is malformed and cannot be
	// unmarshal to a given type.
	// It happens when a request or a response type is modified in the request processing pipeline.
	ErrUnmarshal = errors.New("mob: failed to unmarshal")
)

Functions

func AddInterceptor added in v1.3.0

func AddInterceptor(interceptor Interceptor)

AddInterceptorTo adds an Interceptor to the global Mob instance. Interceptors are invoked in order they're added to the chain.

func AddInterceptorTo added in v1.3.0

func AddInterceptorTo(m *Mob, interceptor Interceptor)

AddInterceptorTo adds an Interceptor to the given Mob instance. Interceptors are invoked in order they're added to the chain.

func Notify

func Notify[T any](ctx context.Context, event T) error

Notify dispatches a given event and execute all handlers registered with a dispatched event's type. Handlers are executed concurrently and errors are collected, if any, they're returned to the client.

If there is no appropriate handler in the global Mob instance, ErrHandlerNotFound is returned.

func RegisterEventHandler

func RegisterEventHandler[T any](hn EventHandler[T], opts ...Option) error

RegisterEventHandler adds a given event handler to the global Mob instance. Returns nil if the handler added successfully, an error otherwise.

Multiple event handlers can be registered for a single event's type.

func RegisterEventHandlerTo added in v1.1.0

func RegisterEventHandlerTo[T any](m *Mob, ehn EventHandler[T], opts ...Option) error

RegisterEventHandlerTo adds a given event handler to the given Mob instance. Returns nil if the handler added successfully, an error otherwise.

Multiple event handlers can be registered for a single event's type.

func RegisterRequestHandler

func RegisterRequestHandler[T any, U any](hn RequestHandler[T, U], opts ...Option) error

RegisterRequestHandler adds a given request handler to the global Mob instance. Returns nil if the handler added successfully, an error otherwise.

An only one handler for a given request-response pair can be registered. If support for multiple handlers for the same request-response pairs is needed within the Mob global instance, introduce type aliasing to avoid handlers' collision.

func RegisterRequestHandlerTo added in v1.1.0

func RegisterRequestHandlerTo[T any, U any](m *Mob, rhn RequestHandler[T, U], opts ...Option) error

RegisterRequestHandlerTo adds a given request handler to the given Mob instance. Returns nil if the handler added successfully, an error otherwise.

An only one handler for a given request-response pair can be registered. If support for multiple handlers for the same request-response pairs is needed within the same Mob instance, introduce type aliasing to avoid handlers' collision.

func Send

func Send[T any, U any](ctx context.Context, req T) (U, error)

Send sends a given request T to an appropriate handler and returns a response U.

If the appropriate handler does not exist in the global Mob instance, ErrHandlerNotFound is returned.

Types

type AggregateHandlerError

type AggregateHandlerError []error

An AggregateHandlerError is a type alias for a slice of handler errors. It applies only to event handlers.

func (AggregateHandlerError) Error

func (e AggregateHandlerError) Error() string

func (AggregateHandlerError) Is

func (e AggregateHandlerError) Is(target error) bool

type EventHandler

type EventHandler[T any] interface {
	Handle(ctx context.Context, event T) error
}

EventHandler provides an interface for an event handler.

type EventHandlerFunc added in v1.2.0

type EventHandlerFunc[T any] func(ctx context.Context, event T) error

EventHandlerFunc type is an adapter to allow the use of ordinary functions as event handlers.

func (EventHandlerFunc[T]) Handle added in v1.2.0

func (f EventHandlerFunc[T]) Handle(ctx context.Context, event T) error

type EventNotifier added in v1.1.0

type EventNotifier[T any] interface {
	// Notify dispatches a given event and execute all handlers registered with a dispatched event's type.
	// Handlers are executed concurrently and errors are collected, if any, they're returned to the client.
	//
	// If there is no appropriate handler in the notifier's Mob instance, ErrHandlerNotFound is returned.
	Notify(ctx context.Context, event T) error
}

EventNotifier is the interface that wraps the mob's Notify method.

func NewEventNotifier added in v1.1.0

func NewEventNotifier[T any](m *Mob) EventNotifier[T]

NewEventNotifier returns an event notifier which uses a given Mob instance.

type Interceptor added in v1.3.0

type Interceptor func(ctx context.Context, req interface{}, invoker SendInvoker) (interface{}, error)

Interceptor intercepts an invocation of a Send method.

type Mob added in v1.1.0

type Mob struct {
	// contains filtered or unexported fields
}

A Mob is a request / event handlers registry.

func New added in v1.1.0

func New() *Mob

New returns an initialized Mob instance.

type Option added in v1.2.0

type Option interface {
	// contains filtered or unexported methods
}

Option configures a handler during the registration process.

func WithName added in v1.2.0

func WithName(name string) Option

WithName returns an Option that associates a given name with a handler.

type RequestHandler

type RequestHandler[T any, U any] interface {
	Handle(ctx context.Context, req T) (U, error)
}

RequestHandler provides an interface for a request handler.

type RequestHandlerFunc added in v1.2.0

type RequestHandlerFunc[T any, U any] func(ctx context.Context, req T) (U, error)

RequestHandlerFunc type is an adapter to allow the use of ordinary functions as request handlers.

func (RequestHandlerFunc[T, U]) Handle added in v1.2.0

func (f RequestHandlerFunc[T, U]) Handle(ctx context.Context, req T) (U, error)

type RequestSender added in v1.1.0

type RequestSender[T any, U any] interface {
	// Send sends a given request T to an appropriate handler and returns a response U.
	//
	// If the appropriate handler does not exist in the sender's Mob instance, ErrHandlerNotFound is returned.
	Send(ctx context.Context, req T) (U, error)
}

RequestSender is the interface that wraps the mob's Send method.

func NewRequestSender added in v1.1.0

func NewRequestSender[T any, U any](m *Mob) RequestSender[T, U]

NewRequestSender returns a request sender which uses a given Mob instance.

type SendInvoker added in v1.3.0

type SendInvoker func(ctx context.Context, req interface{}) (interface{}, error)

SendInvoker is a function called by an Interceptor to invoke the next Interceptor in the chain or the underlying invoker.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL