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