Seguro como muchos de vosotros, lectores trabajáis actualmente con un lenguaje diferente a Go en vuestro día a día, véase Java, PHP, etc. Y posiblemente estarás acostumbrado ya a toda la vertiente que hay detrás de utilizar, Domain Driven Design y arquitectura hexagonal o quizás te suene mucho de escucharlo entre compañeros.
Pues hoy queremos traer uno de esos palabros mágicos que surgen en conversaciones dentro de estos ámbitos, el Command Bus, puede que ya estes familiarizado con este concepto y no sepas como aplicarlo en Go o ni siquiera tengas idea de como funciona, tranquilos que vamos a explicar en que consiste y como se resuelve en Go.
¿Qué es un Command?
No podemos hablar de un Command Bus sin explicar primero uno de sus componentes los Command o Comandos. Los Commands se utilizan para separar los aspectos técnicos de la entrada de datos producidas por los usuarios de lo que viene a ser nuestro dominio. Un Command en Go lo vamos a representar como cualquier otro struct
de nuestro código, no hay ningún tipo de magia asociado a nuestro Command
.
Un Command
va a representar una la información que necesitaremos tener para realizar nuestra acción (use case. Por ejemplo AddGopher
.
Así pues imaginemos como decimos que tendremos un use case
que se encargará de añadir gophers en nuestro sistema, entonces nuestro Command
podría ser tal que así:
type AddGopherCommand struct {
ID string
Name string
Color string
}
Así pues en nuestro Controller
podríamos empezar a utilizar nuestro Command
como un DTO
que construiremos con los datos recibidos por el usuario.
type GopherHandler struct {
// some dependencies
}
type addGopherRequest struct {
ID string `json:"id"`
Name string `json:"name"`
Color string `json:"image"`
}
func (h Handler) AddGopher(w http.ResponseWriter, r *http.Request) {
// decode request body
cmd := AddGopherCommand{
ID: req.ID,
Name: req.Name,
Color: req.Color
}
...
}
Supongo que hasta ahora os habréis quedado igual, ya que esta claro que ahora tenemos un Command
con nuestros inputs, que previamente han podido ser validados por ejemplo, pero eso y pasarle estos datos a nuestro servicio de aplicación sería casi lo mismo ¿no? Aquí es donde entra en juego nuestro amigo Command Bus
, el cual es el único que va a esperar un Command
argumento.
Si cambiamos un poco el código anterior podríamos ver cómo quedaría.
type GopherHandler struct {
commandBus CommandBus
}
type addGopherRequest struct {
ID string `json:"id"`
Name string `json:"name"`
Color string `json:"image"`
}
func (h Handler) AddGopher(w http.ResponseWriter, r *http.Request) {
// decode request body
cmd := AddGopherCommand{
ID: req.ID,
Name: req.Name,
Color: req.Color
}
h.commandBus->Exec(cmd)
}
Ahora nuestro controlador es capaz de ejecutar nuestra acción despreocupándose por completo de cuál será todo el funcionamiento y sus dependencias para funcionar.
Antes de entrar en como funciona la magia de nuestro Command Bus
, veamos algunas de las ventajas que nos da el utilizar los Commands
.
Quizás la ventaja más característica que podríamos ver es que podríamos utilizar el Command
en cualquier cliente, y siempre que lo acompañemos del Command Bus
sabremos que hará exactamente lo que esperamos que haga.
Y por otro lado, tal y como hemos comentado, hemos liberado a nuestro controlador de muchas de las inyecciones de dependencias que necesitaría de otro modo.
Pero todo esto no es magia, sino que nuestro Command Bus
a través del Command
recibido nos pone en contacto con el Command Handler
que se encargará de realizar dicha acción.
Command Handler
Sé que estáis deseando entrar en la magia del Command Bus
pero creo que es mejor que vayamos primero a los elementos que lo componen para que luego esa magia, no sea tanta.
Y es que cuando nuestro Command Bus
ejecute el método handle
lo que hará internamente es llamar al Command Handler
que tiene registrado. El Command Handler
será el encargado de orquestar la acción que queremos llevar a cabo, encargándose de las dependencias pertinentes.
Así pues si queremos crear un Command Handler
para nuestro anterior Command
tendríamos algo como esto:
type AddGopherCommandHandler struct {
repository Repository
}
func NewAddGopherCommandHandler(repository) AddGopherCommandHandler {
return AddGopherCommandHandler{repository: repository}
}
func (ch AddGopherCommandHandler) Handle(c AddGopherCommand) error {
// do some here
}
Como podemos ver no es nada que no hayamos visto en otra ocasión, simplemente tenemos un struct
que espera tener las dependencias para funcionar, véase, repositorios, application services, etc. Un constructor, que inicializará nuestro Command Handler
y un método Handle que será la que llamaremos para ejecutar nuestra acción.
Como hemos dicho, el método Handle
será el encargado de realizar la acción que estamos buscando, devolviendo simplemente un error si el proceso no fuera correcto.
Command Bus
Antes de empezar, vamos a partir de una serie de bases para construir nuestro Command Bus
, si buscáis en internet veréis que muchas librerías de Command Bus
funcionan utilizando reflection, ya que la falta de generics en Go obliga a que para poder trabajar con estructuras dinámicas tengamos que recurrir a él.
Pero yo quiero ir un paso más allá y evitarlo lo más posible en nuestra implementación, para ello habrá una serie de boilerplate, que no podremos evitarnos realizar, pero habremos ganado en rendimiento.
Ahora que ya conocemos los elementos que componen nuestro Command Bus
veamos cómo crearlo.
Primero que nada vamos a necesitar un Command
.
type Command interface {
CommandID() string
}
Como habréis supuesto, nuestro Command
lo podremos resolver con una interface
pero para evitar el uso de interface{}
deberemos hacer un pequeño truco, y es dotarle de un comportamiento, es decir, crearemos un método CommandID()
que será el identificador que utilizaremos para mapear con nuestro Command Handler
.
A continuación sabemos que necesitamos de un Command Handler
, y como vimos un Command Handler
no deja de ser una función que espera un Command
y devuelve un error
. Así pues crearemos una interfaz que cumpla con dicho contrato, es decir Handle(Command) error
type CommandHandler interface {
Handle(Command) error
}
Perfecto, ahora necesitamos crear nuestro Command Bus
, sabemos que nuestro Command Bus
será el encargado de cuando reciba como parámetro un Command
en su método Handle
buscar entre los Command Handlers
registrados y ejecutar la función de dicho Command Handler
.
Es decir necesitaremos que tenga un map
que vincule un Command
con un Command Handler
.
type CommandBus struct {
handlersMap map[string]CommandHandler
}
func NewCommandBus() CommandBus {
return CommandBus{handlersMap: make(map[string]CommandHandler)}
}
¿Pero no habías dicho vincular un Command
, con su Command Handler?
¿Por qué un string
? Muy buena pregunta, antes de responder a esto, os quiero enseñar el método que se encargará de registrar los elementos en el map
.
func (cb *CommandBus) RegisterHandler(c Command, ch CommandHandler) error {
cmdID := c.CommandID()
_, ok := cb.handlersMap[cmdID]
if ok {
return fmt.Errorf("the Command %s is already register", cmdID)
}
cb.handlersMap[cmdID] = ch
return nil
}
Vayamos por partes, el RegisterHandler
espera dos parámetros, a saber, el Command
y el CommandHandler
, ¿qué sucede entonces? si creáramos un map
de Command
como key
, no podríamos posteriormente acceder a él debido a que el Command
que registraríamos sería diferente al que vamos a usar, simplemente porque el RegisterHandler
se hace en el momento de levantar nuestra aplicación, y la llamada al Handle
durante su ejecución, lo veremos más claro en el ejemplo de como funcionan todas las piezas.
Aquí nos aprovecharemos del método CommandID()
que sabemos que tienen nuestros Command
para obtener la clave que nos servirá para realizar dicho mapeo.
Como veis el método es sencillo, simplemente comprueba que no se haya registrado dos veces el mismo Command
y de no tener error lo guarda en nuestro map
. Gracias a esto nuestro Command Bus
ya tiene forma de saber que un Command
va unido a su Command Handler
.
Así pues sólo nos queda ver como funciona el método Exec(Command) error
func (cb CommandBus) Exec(c Command) error {
cmdID := c.CommandID()
ch, ok := cb.handlersMap[cmdID]
if !ok {
return fmt.Errorf("there not any CommandHandler associate to Command %s", cmdID)
}
return ch.Handle(c)
}
Primeramente, volvemos a obtener el nombre del Command
para buscarlo en el map
, una vez lo tenemos, simplemente comprobamos si dicho Command
está registrado de no ser así devolvemos error
y sino pues ejecutamos el Handle
.
Si recordamos nuestro Command Handler
sólo era un type custom
de func(Command) error
, con lo cual lo único que tendremos que hacer es ejecutar dicha función que hemos registrado.
¿Cómo funciona el Command Bus?
Ya hemos creado el Command Bus
pero aún tenemos que hacer unos pequeños ajustes para que todo funcione correctamente, si recordamos nuestro Command Handler
aceptaba un Command
concreto, uno de los cambios que tendremos que realizar será aquí ya que para evitar el uso de interface{}
y reflection
hemos tenido que adoptar una solución que como decíamos, nos llevará a generar un poco de código repetitivo.
Anteriormente nuestro Command Handler
lucía así:
type AddGopherCommandHandler struct {
repository Repository
}
func NewAddGopherCommandHandler(repository) AddGopherCommandHandler {
return AddGopherCommandHandler{repository: repository}
}
func (ch AddGopherCommandHandler) Handle(c AddGopherCommand) error {
// do some here
}
El cambio sólo será sobre el método Handle
ya que debe implementar el tipo CommandHandler
que era una interfaz tal que, Handle(Command) error
, esto nos va a obligar a dejar de conocer el Command
recibido y tener que hacer un type assertion
para asegurarnos de que el Command
recibido es el esperado.
func (ch AddGopherCommandHandler) Handle(c Command) error {
cmd, ok := c.(AddGopherCommand)
if !ok {
return errors.New("Invalid command")
}
// do some here
}
Como veis evitar el uso de reflection
nos lleva a tener que realizar un cast
a nuestro Command
que por suerte sabemos que nuestro Handle
sólo espera ese tipo de Commands
con lo cual tampoco resulta muy dramático.
Por otro lado para que nuestro Command
cumpla la interfaz Command
, deberemos añadir el método CommandID()
.
type AddGopherCommand struct {
ID string
Name string
Color string
}
func (g AddGopherCommand) CommandID() string {
return "gopher_AddGopherCommand"
}
Ahora ya lo tenemos todo listo para poner en funcionamiento nuestro Command
, primero que nada en allí donde realicemos el levantar nuestro servicio o aplicación, crearemos todas las dependencias e inyecciones.
cb := NewCommandBus()
repository := mongo.NewRepository()
addGopherCommandHandler := NewAddGopherCommandHandler(repository)
err := cb.RegisterHandler(AddGopherCommand{}, addGopherCommandHandler)
if err != nil {
log.Fatal(err)
}
gopherController := NewGopherController(cb)
...
Ahora ya nuestro controlador está listo para funcionar como vimos anteriormente, cuando recibamos una petición al endpoint
deseado, simplemente llamará a nuestro Command Bus
que se encargará de realizar la acción deseada en este caso crear un Gopher
.
Conclusión
Ya hemos llegado al final de este extenso artículo, si quieres profundizar aún más te aconsejo que le eches un ojo a los artículos de Matthias Noback, los ejemplos son en PHP, pero sin duda toda la teoría que lo rodea es realmente interesante.
Ahora ya sabes cómo esconder toda esa lógica y dependencias y reducir mucho la responsabilidad de nuestros clientes, dejándolo todo a merced de las herramientas encargadas de gestionar dichas acciones por ti.
Como siempre si tenéis cualquier duda o comentario, podéis dejarlo abajo en la cajita dedicada para ello o vía Twitter, en @FriendsOfGoTech