A lightweight implementation of the Mediator Pattern for GoLang
, inspired by jbogard's MediatR framework for .net.
This project was previously known as go-mediator
. It has been renamed as mediator
for consistency with the package name and because all blugnu
projects are golang; the go-
prefix was just noise.
At the same time, the project was completely re-written; it now shares little more than the original concept with the previous incarnation.
If you previously imported go-mediator
you should update your imports to the renamed module.
The Mediator is a simple pattern that uses a 3rd-party (the mediator) to facilitate communication between two other parties without either requiring knowledge of each other.
It is a powerful pattern for achieving loosely coupled code.
There are many ways to implement the pattern, from simple func
pointers to sophisticated and complex messaging systems; blugnu/mediator
sits firmly at the simple end of that spectrum!
For code that provides a substantial component of domain behaviour, using mediator
provides a consistent mechanism for de-coupling, implementing, calling and mocking those components.
Often when testing you may find yourself needing to use a function variable so that you can inject a fake or spy function in order to test higher-level code. mediator
is not designed or intended to replace this or similar techniques.
- it is not a message queue
- it is not asynchronous
- it is not complicated!
Your code registers commands to respond to requests of various types. Commands are then called by passing requests to the mediator; the mediator lookups up the command that handles that request, calls it and returns the result and any error.
blugnu/mediator
maintains a registry of commands that respond to requests of a specific type. As well as responding to a specific request type, each registered command identifies the result type that it returns to any caller.
There can be only one command registered for any given request type.
Commands are registered during initialising of your application using RegisterCommand
, or by establishing mock commands in tests. Command configuration checks are performed when registering commands. The RegisterCommand
function tests for an implementation of the ConfigurationChecker
interface (CheckConfiguration()
function) which is called if present. If configuration checks return an error, this is returned by the RegisterCommand
function and the command is not registered.
Registered commands are called indirectly via a generic mediator.Execute[TRequest, TResult]
function: the mediator.
The mediator consults the registered commands to identify the command for the request type involved. If no command is registered then a NoCommandForRequestTypeError
is returned.
If a command is identified but the caller and the command do not agree on the result type, a ResultTypeError
is returned.
If the correct result type is expected, the mediator tests for an implementation of the Validator
interface (Validate()
function) which is called if present. Any error returned from the Validate()
function is wrapped in a ValidationError
(if necessary) and returned to the caller.
If there is no Validator
interface, or the request is validated successfully, the request is passed to the command and the result and any error from the command then returned to the caller.
All of this takes place synchronously as direct function calls. i.e. if the command panics, the stack will contain a complete path of execution from the caller, thru the mediator to the corresponding command function.
- (Recommended): Create a Package for Your Command
- Declare request, result and command types
- (Optional) Implement the
ConfigurationChecker
interface for the command - (Optional) Implement the
Validator
interface for the command - Implement the
CommandHandler
interface for the command
- There are numerous advantages to implementing each command in its own package. See Packaged Commands for more details.
- Any configuration checks incorporated in the
Execute
function are performed for every request; performing these checks in aCheckConfiguration()
function (implementing theConfigurationChecker
interface) these checks are performed just once, at the time of registering the command. See Command Configuration Checks for more information.
- Any request validation is recommended to be performed in a
Validate()
function (implementing theValidator
interface). See Request Validation for more information.
- Register the command, e.g.:
err := mediator.RegisterCommand[myCommand.Request, *myCommand.Result](ctx, &myCommand.Handler{})
Once a command has been registered it cannot be unregistered, i.e. it is not possible to dynamically reconfigure registered commands to respond to requests of a given type with different commands at different times. This is by design. In contrast, mock commands can (and must) be reconfigured during the execution of different tests, and this is possible (see: Testing With Mediator).
The mediator.Execute
function accepts a Context
, the request to be executed and a pointer to a value of the result type. The function returns the result value and any error from the command.
The result type pointer is not de-referenced by the mediator and does not receive any result.
The pointer is required only as a type-hint for the compiler so that it can infer the types required by the generic Execute
function.
It is recommended to use new()
to provide a pointer of the required type
rq := myCommand.Request{Id: id}
rs, err := mediator.Execute(ctx, rq, new(*myCommand.Result))
In the above example,
myCommand
returns a pointer to amyCommand.Result
;new()
in this case is used to return a pointer to a pointer.
For commands that have no result value mediator
provides a convenience type for use when implementing and registering commands returning no result, and a variable for use as a type-hint when calling such a command:
type NoResultType *int
var NoResult = new(NoResultType)
A command that specifically has no result value is registered with a result type of mediator.NoResultType
and, as you would expect, the Execute()
function of that command returns mediator.NoResultType
.
NoResultType
is a pointer so that when implementing theExecute()
function for a command returningNoResultType
you can returnnil
.
// Registering a command returning no result
err := mediator.RegisterCommand[MyRequestType, mediator.NoResultType](ctx, MyCommandHandler{})
// Implementing the Execute function of a command returning no result
func (cmd *Handler) Execute(ctx context.Context, req Request) (mediator.NoResultType, error) {
if err := SomeOperation(); err != nil {
return nil, err
}
return nil, nil
}
A caller can use either new(mediator.NoResultType)
or mediator.NoResult
as the result type-hint for the Execute
function, discarding the returned result.
rq := deleteFoo.Request{Id: id}
// these two statements are functionally equivalent
_, err := mediator.Execute(ctx, rq, mediator.NoResult)
_, err := mediator.Execute(ctx, rq, new(mediator.NoResultType))
Before executing any request, a command will typically check the configuration of the command, e.g. to ensure that any required dependencies have been supplied. This incurs the overhead of those configuration checks on every request when they typically only need to be performed once.
To perform these checks only once, a command may implement the ConfigurationChecker
interface:
type ConfigurationChecker interface {
CheckConfiguration(context.Context) (err error)
}
If implemented, the CheckConfiguration
function is called when registering the command. If an error is returned from the function then the command registration fails and the error is returned from the RegisterCommand
function.
The loose-coupling that can be achieved with a mediator is particularly useful for unit testing.
When unit testing code that calls some command using mediator you are able to mock responses to the request to test the behaviour of your code under a variety of error or result conditions, without having to modify the code under test.
You can implement mock commands for your request as needed, or you can use the mock factories provided by blugnu/mediator
; these should be sufficient for most - if not all - common use cases.
The mocks returned by these factories provide an Unregister()
method to remove the registration for that command; typically you would defer a call to this Unregister()
method immediately after initialising the mock
mock := mediator.MockCommand[myCommand.Request, myCommand.Result]()
defer mock.Unregister()
The example above illustrates the mock factory that initialises a command that mocks a successful call, returning a zero-value result and nil error.
The factory functions are:
// Mocks a command returning a zero-value result and nil error
MockCommand[TRequest, TResult]() *mockcommand[TRequest, TResult]
// Mocks a command returning a specific result and nil error
MockCommandResult[TRequest, TResult](result TResult) *mockcommand[TRequest, TResult]
// Mocks a command returning a specific error
MockCommandError[TRequest, TResult](error) *mockcommand[TRequest, TResult]
// Mocks a command returning an error from an implementation
// of the Validator interface
MockCommandValidationError[TRequest, TResult](error) *mockcommand[TRequest, TResult]
There is no factory for mocking a command that returns an error from a
ConfigurationChecker
interface; such a command would be impossible to register and so could not be called in any test scenario.
The mock returned by these factories provide methods for determining how many times the mock was called, whether it was called at all, as well as copies of all requests received by the mock over its lifetime.
If the provided mock factories are not sufficient, you can register a custom mock using the RegisterMockCommand()
function. This is similar to the RegisterCommand()
function, registering the specified command to handle requests of a specified type and returning a specified result type.
There are two main differences:
RegisterMockCommand()
does not return any error; if the supplied mock returns an error from any configuration checks, the mock will not be registered and the function willpanic
.RegisterMockCommand()
returns a function to be used to unregister the mock when no longer required (typically immediately deferred to clean up the registration when the test completes)
unreg := RegisterMockCommand[myCommand.Request, NoResultType](ctx, &mockMyCommand{})
defer unreg()