Supongo que no os coge de nuevo el tema de gestión de dependencias, pues lo llevamos realizando desde mucho tiempo en distintos lenguajes de programación. Véase Java o Php, entre otros. y además todos estos lenguajes nos ofrecían herramientas para poder gestionar dichas dependencias de una manera sencilla y desacoplada de nuestra aplicación en sí.
En Go supongo que os resultará realmente familiar encontrarnos con un main similar al de GopherApi:
package main
import (
"flag"
"fmt"
"log"
"net/http"
"github.com/friendsofgo/gopherapi/pkg/removing"
"github.com/friendsofgo/gopherapi/pkg/modifying"
"github.com/friendsofgo/gopherapi/pkg/adding"
"github.com/friendsofgo/gopherapi/pkg/fetching"
"github.com/friendsofgo/gopherapi/pkg/storage/inmem"
sample "github.com/friendsofgo/gopherapi/cmd/sample-data"
gopher "github.com/friendsofgo/gopherapi/pkg"
"github.com/friendsofgo/gopherapi/pkg/server"
)
func main() {
withData := flag.Bool("withData", false, "initialize the api with some gophers")
flag.Parse()
var gophers map[string]gopher.Gopher
if *withData {
gophers = sample.Gophers
}
repo := inmem.NewRepository(gophers)
fS := fetching.NewService(repo)
aS := adding.NewService(repo)
mS := modifying.NewService(repo)
rS := removing.NewService(repo)
s := server.New(fS, aS, mS, rS)
fmt.Println("The gopher server is on tap now: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", s.Router()))
}
Cierto es que podemos esconder todo lo que es la inicialización de nuestro código, pero aún así tendríamos que estar inicializando nosotros a mano, nuestros repositorios, servicios, etc…
En este artículo nos vamos a centrar en cómo realizar esta gestión de dependencias de otra manera mediante la herramienta Wire que está desarrollando actualmente Google, de momento no la recomiendan para su uso en producción pero podemos empezar a ver como funciona. Pero no nos vamos a entrar en como funciona la arquitectura hexagonal y por qué tenemos invertidas nuestras dependencias, o que son los servicios o repositorios, si te faltan esos conocimientos te recomendamos echarle un vistazo al curso de CodelyTV donde lo explican genial, de una manera agnóstica al lenguaje.
Un poco de contexto
Si vienes siguiendo el blog desde hace tiempo sabrás que en un pasado artículo empezamos a desarrollar GopherApi, una API CRUD escrita tan sólo utilizando gorilla mux como librería. La API fue construida para poder seguir utilizándola en diferentes posts como es éste.
Para este artículo hemos modificado la API, pasando a la versión v0.3.0 donde hemos refactorizado nuestro código para tener servicios y crear los endpoints que faltaban.
Esto lo hemos hecho para que tenga algo más de sentido utilizar un gestor de dependencias como Wire
Ahora a grandes rasgos tendremos la siguiente estructura:
type Repository struct {
data map[string]string
}
type Service struct {
repository Respository
}
type Server struct {
service Service
}
Además todos nuestros servicios van a esperar un repositorio:
func NewService(repository Repository) Service{
return Service{repository}
}
Por otro lado nuestro Server esperará esos servicios para poder añadir el comportamiento adecuado a cada uno de los endpoints
Actualmente si queremos inicializar toda esta jerarquía haríamos algo similar a esto en el main.go
repository := NewRepository()
service := NewService(repository)
server := NewServer(service)
server.Run()
Todo el código mostrado es a nivel de ejemplo general para que tengáis los conceptos claros, en GopherApi esa parte sería:
repo := inmem.NewRepository(gophers)
fS := fetching.NewService(repo)
aS := adding.NewService(repo)
mS := modifying.NewService(repo)
rS := removing.NewService(repo)
s := server.New(fS, aS, mS, rS)
log.Fatal(http.ListenAndServe(":8080", s.Router()))
Cómo veis aquí es exactamente lo mismo, pero en este caso nuestros servicios, y server utilizan interfaces en lugar de structs, esto nos permite poder decidir que repositorio vamos a utilizar, siendo en este caso el que hemos creado dentro del paquete inmem
pero podríamos pasar cualquiera que cumpliera con la interfaz.
Como vemos sí tenemos una aplicación muy grande esto podría empezar a convertirse en un monstruo y tendríamos que empezar a utilizar otras técnicas para poder encapuslar toda la inicialización.
Usando Wire
Vamos a ver cómo podemos cambiar nuestro código para que no necesitemos realizar tantos pasos para la inicialización. Para esto como venimos comentando utilizaremos Wire.
Obviamente el primer paso será instalarlo:
$ go get github.com/google/wire/cmd/wire
Si nos basamos en el código de GopherApi pasaremos de:
...
repo := inmem.NewRepository(gophers)
fS := fetching.NewService(repo)
aS := adding.NewService(repo)
mS := modifying.NewService(repo)
rS := removing.NewService(repo)
s := server.New(fS, aS, mS, rS)
...
...
s := InitializeServer()
...
Como podemos ver de un golpe hemos simplificado muchísimo, pero claro, esto también lo podemos hacer pasándolo todo a un simple método.
Ahora nos tocará crear un fichero wire.go
que supongo ya habréis deducido llevará nuestro método InitializeServer
// wire.go
//+build wireinject
func InitializeServer(gophers map[string]gopher.Gopher) server.Server {
wire.Build(server.New, inmem.NewRepository, fetching.NewService, adding.NewService, modifying.NewService, removing.NewService)
return server.NewWire()
}
A este fichero deberemos añadirle el tag //+build wireinject
para que lo ignore a la hora de construir nuestro binario, ya que sólo es el injector que se utilizará para generar el código final.
Algo que no nos ha terminado de gustar de esta parte, es que nos ha obligado a crear un “constructor” vacío para no exponer nuestro struct del paquete server
, también podríamos pasar los servicios como punteros y pasar un nil, ya que realmente solo necesita esta salida para poder calcular el tipo de vuelta.
Como podéis ver además podemos pasar parametros al método y el sólo inferirá quién es el que lo necesita, como en este caso que pasamos los datos de los gophers.
Ahora sólo nos queda hacer que la magia suceda, para ello entramos en la carpeta donde hemos creado nuestro wire.go
y ejecutamos:
$ wire
Y si todo ha ido bien nos habrá generado un fichero similar a este:
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
import (
"github.com/friendsofgo/gopherapi/pkg"
"github.com/friendsofgo/gopherapi/pkg/adding"
"github.com/friendsofgo/gopherapi/pkg/fetching"
"github.com/friendsofgo/gopherapi/pkg/modifying"
"github.com/friendsofgo/gopherapi/pkg/removing"
"github.com/friendsofgo/gopherapi/pkg/server"
"github.com/friendsofgo/gopherapi/pkg/storage/inmem"
)
// Injectors from wire.go:
func InitializeServer(gophers map[string]gopher.Gopher) server.Server {
repository := inmem.NewRepository(gophers)
service := fetching.NewService(repository)
addingService := adding.NewService(repository)
modifyingService := modifying.NewService(repository)
removingService := removing.NewService(repository)
serverServer := server.New(service, addingService, modifyingService, removingService)
return serverServer
}
Si ahora ejecutamos nuestra aplicación funcionará correctamente, pero hay un pero, al crear otro fichero dentro del propio package main cuando lanzamos el comando go run
tendremos que indicarle que también cargue el fichero wire_gen.go
$ go run cmd/gopherapi/wire_gen.go cmd/gopherapi/main.go --withData
Pero, recordemos que hace poco hablamos de como estructurar nuestros proyectos en Go y de la existencia del paquete internal, vamos a ver como quedaría entonces todo:
// main.go
...
s := container.InitializeServer()
...
De esta manera nuestro wire_gen.go
pertenecerá al paquete internal/container
desde el cual podremos regenerarlo todas las veces que queramos, y además ya no nos molesta a la hora de compilar ni ejecutar go run
Como vemos los chicos de Google han decidido tirar de código generado, más que hacer magia con reflection o similar, para generar el código que al final nosotros mismos picaríamos a mano. Nos ahorra mucho tiempo y deja el main.go
bastante más liberado.
Para ver el código completo de GopherAPI visitad la versión v0.3.1
Si queréis profundizar más os recomendamos que visitéis la documentación oficial y recordad cualquier duda nos la podéis dejar en los comentarios o en nuestro Twitter @FriendsofGoTech