Objetivo
Cuando comencé a programar en go, me hice muchas preguntas. Preguntas sobre el lenguaje en sí, buenas prácticas, code conventions, naming conventions, cómo escribir código idiomático, etc,... pero creo que una de mis primeras preguntas fue: ¿cómo estructuro mi aplicación?. Luego de haber investigado y haber escrito un par de aplicaciones productivas, mi idea es compartirles una propuesta de cómo estructurar una aplicación en golang (o simplemente go) y dejarles un ejemplo en GitHub fácil de utilizar.
Si buscan en internet realmente hay muchas, muchas, propuestas e ideas (al final comparto algunos links). Muchas de esas basadas en la propuesta de Clean Architecture de Uncle Bob y en Domain Driven Design. En este artículo les comparto una propuesta más, basada en clean architecture, inspirada en artículos que he leído y en español :).
Clean Architecture? y eso?
Como mencionaba, en este gran artículo de hace ya varios años, Uncle Bob describe cuáles considera que deben ser las características de una buena estructura o modularización de una aplicación.
Intentaré describir en español (o spanglish) y usando un poco mis palabras lo que se menciona en el artículo.
Las aplicaciones deben ser:
- Independientes de frameworks: la arquitectura no debe tener dependencias con frameworks subyacentes que utilicemos. Esto nos permite flexibilidad ante cambios de framework y no atarnos a las restricciones que cada uno presente .
- Testeable: los componentes de la aplicación deben ser testeables, sin necesidad de otros módulos o de tener una UI.
- Independiente de la UI: la interfaz con el usuario de la aplicación podría ser un command line, una API, una web y eso debería ser fácilmente intercambiable sin cambiar las reglas de negocio.
- Independiente de la base de datos: la forma en que se almacenan los datos debe ser independiente de tus reglas de negocio. Si la aplicación comienza almacenando datos en mysql y luego evoluciona y debe simplemente hacer un POST de esa información a un API externa, deberían ser intercambiables sin modificar reglas de negocio.
- Independiente de cualquier ente externo: basicamente las reglas de negocio no deberían saber de nada de lo que sucede por fuera de ellas, ni de DBs, ni de APIs o de cualquier otra cosa.
Siguiendo estos principios según Uncle Bob podemos separar nuestro código en 4 capas:
- Entities: esta capa encapsula las reglas de negocio de nuestro dominio. Normalmente en go estas entidades son representadas por structs y sus funciones asociadas.
- Use cases: esta capa contiene reglas de negocio específicas de nuestra aplicación. Aquí se proveen los servicios para cumplir los casos de uso.
- Interface Adapters / Controller: esta capa consiste en una serie de adaptadores que toman los datos en el formato que los envía el usuario y los transforma para que puedan ser utilizados por nuestra capa de casos de uso.
- Framework & Driver: esta capa está generalmente compuesta de conectores o frameworks que nos permiten adaptarnos a distintos entes externos, como base de datos, APIs, etc.
Ahora que conocemos más de clean architecture, apliquemos estos principios y separación en capas a una aplicación en go.
Aplicando Clean Architecture en Go
Utilizaremos como base de la explicación una API de mensajes. ¿Por qué una API de mensajes? Solo porque quise salir de la clásica aplicación de los ejemplos que manejan usuarios 👨🏻💻. La API que utilizaremos maneja una entidad Message
que solo tiene como dato un string, siendo ese string el texto del mensaje (si lo sé, me maté con el ejemplo 😅). Mi idea no es centrarme en una aplicación compleja a nivel lógica de negocio, sino simplemente usarla para mostrar su estructura.
Si visualizamos el proyecto en GitHub o descargamos el ejemplo, la carpeta root se verá más o menos así:
drwxr-xr-x 14 hlopez staff 448 Nov 3 16:17 .
drwxr-xr-x 3 hlopez staff 96 Oct 25 18:10 ..
drwxr-xr-x 5 hlopez staff 160 Oct 30 19:30 api
-rw-r--r-- 1 hlopez staff 198 Oct 25 19:39 main.go
drwxr-xr-x 5 hlopez staff 160 Nov 6 18:12 message
drwxr-xr-x 4 hlopez staff 128 Oct 30 20:56 restclient
...
...
...
Como podemos ver, tenemos básicamente 3 módulos api
, message
y restclient
. Existen algunos módulos más en la aplicación, pero son utilitarios y no son relevantes estructuralmente hablando.
Relacionando los módulos con las capas de clean architecture :
- api - contiene el código que se ubica en la capa Controller
- message - este módulo contiene código de la capa Entities y de la capa Use cases.
- ** restclient - contiene código que se ubica en la capa Framework & drivers.
** El módulo restclient es solo un ejemplo, pero podríamos tener más módulos en la capa "Frameworks & drivers" como accesos a mysql, a elastic search, a key value stores (como cassandra), etc.
La aplicación puede ser iniciada haciendo simplemente go run main.go
. El código en main.go
es extremadamente sencillo.
main.go
package main
func main() {
// Dependency injection section
clients := config.RestClients()
msgAPI := restclient.NewMessageAPI(clients[config.MessageAPI])
msgRepo := message.NewRepository(msgAPI)
msgSrv := message.Service(msgRepo)
msgCtrl := api.NewMessageController(msgSrv)
// Creates the API intance
api := api.New(msgCtrl)
// Runs the application
api.Run()
}
Como se puede ver, la función main()
es la encargada de inicializar los módulos y sus dependencias, para luego iniciar la aplicación usando la función Run()
.
En la siguientes secciones describo el por qué de la existencia de cada módulo (comenzando con el módulo api). La idea es explicar brevemente las implementaciones y hacia el final ver cómo esta arquitectura nos beneficia en el unit testing.
En todos los snippets de código no se muestra el código completo, con imports y funciones auxiliares por cuestiones de simplicidad. El código completo está disponible en GitHub
Módulo "api"
El módulo api está en la capa Controller. Su objetivo es proveer al usuario medios de interactuar con la capa de servicios (Use cases). En este caso, la interfaz para el usuario es de tipo REST API y está implementada utilizando en el framework gin-gonic.
api/api.go
package api
type API struct {
pingCtrl *pingCtrl
msgCtrl MessageController
}
func (api *API) Run() {
r := gin.Default()
api.configRoutes(r)
r.Run()
}
func (api *API) configRoutes(r *gin.Engine) {
r.GET("/ping", func(c *gin.Context) { api.pingCtrl.Ping(c) })
r.GET("/messages/:id", func(c *gin.Context) { api.msgCtrl.Get(c) })
}
El tipo API
tiene como propiedades los distintos controllers que manejarán el input del usuario y lo transformarán para enviar a la siguiente capa. Un ejemplo, es el caso de message controller definido con el tipo messageCtrl
en el archivo messagectrl.go
.
api/messagectrl.go
package api
type MessageController interface {
Get(c Ctx)
}
// messageCtrl handles message entity
type messageCtrl struct {
srv message.Service
}
func (ctrl *messageCtrl) Get(c Ctx) {
// 1 - Unmashall user parameters from gin.Context
id := c.Param("id")
// 2 - Check and handle validation errors (no business validation)
// 3 - Make what you need with your request. In this case, get the message by ID.
msg, err := ctrl.srv.Get(id)
...
...
}
En este ejemplo, la función Get
asociada a messageCtrl
toma los parámetros del usuario, los valida y luego los envia a la capa de servicios. Ignoremos en este caso el uso de la variable srv
del tipo Service
que veremos en la siguiente sección.
Módulo message
Para comenzar, es bueno mencionar que el nombre del módulo refiere al domino de la aplicación, en este caso "mensajes". En este módulo se definen los objetos de dominio que se manejarán y las funciones que implementarán los casos de uso.
En este caso la entidad a manejar es Message
.
message/message.go
package message
type Message struct {
Text string `json:"text"`
}
Extremadamente simple, solo contiene un string que almacena el texto del mensaje. Esta es la entidad que se expone hacia afuera para uso de la capa Controller.
Dentro del módulo message, se tiene también el tipo Service
. Esta interfaz expone las funciones que implementan los casos de uso (capa de Use cases). En el ejemplo de la API de mensajes el caso de uso es muy simple: Obtener un mensaje por ID.
message/service.go
package message
type Service interface {
Get(ID string) (*Message, error)
}
type messageSrv struct {
repo Repository
}
func (srv *messageSrv) Get(id string) (*Message, error) {
// validate parameters depending your business logic
// example: ID must be an UUID v4
// business logic goes here ...
// retrieve message using its repository
msg, err := srv.repo.Get(id)
// do some stuff
return msg, err
}
Notar que Service
no tiene mayores dependencias más que con la interfaz Repository
(tipo que describiremos en breve). Eso hace que la lógica de negocio sea agnóstica a cualquier cambio a nivel almacenamiento.
En la función Get
asociada al tipo messageSrv
se deben validar los parámetros y luego ejecutar las reglas de negocio correspondientes. En este caso podría ser tan simple como obtener el mensaje desde su repositorio.
Respecto al tipo Repository
es una interfaz que brinda una abstracción del acceso a datos. Este código se podría considerar también en la de capa Use cases, en donde los casos de uso que se implementan son los más básicos como: obtener o guardar un Message
.
message/repository.go
package message
type Repository interface {
Get(id string) (*Message, error)
}
type msgRepo struct {
// It can be easly changed by a database or other storage without touching
// business logic
api restclient.MessageAPI
}
func (repo *msgRepo) Get(id string) (*Message, error) {
// Getting message from the external API.
msg, err := repo.api.Get(id)
if err != nil {
return nil, err
}
return build(msg), nil
}
Conceptualmente los repositorios de datos deben interactuar con manejadores de base de datos o conectores con APIs para obtener, almacenar o eliminar información. En el caso del ejemplo, el repositorio realiza una conexión con una API externa que brinda mensajes. En la función Get
del tipo msgRepo
se hace uso del módulo restclient
para lograr dicha conexión.
Módulo "restclient"
El módulo restclient está en la capa Framework & driver. En esta capa la idea es brindar conectores con frameworks u otro entes externos logrando que posibles cambios entre conectores no afecten a la lógica de negocio. Como ya hemos repasado, este es solo un ejemplo, pero podríamos tener más módulos en la capa "Frameworks & drivers" como accesos a mysql, a elastic search, a key value stores (como cassandra), etc.
En este caso, en el módulo restclient se implementa el acceso a una API utilizando el framework resty.
restclient/messageapi.go
package restclient
type MessageAPI interface {
Get(id string) (*Message, error)
}
type msgAPI struct {
restAPI
}
...
...
// Get a message from our external Message API
func (api *msgAPI) Get(id string) (*Message, error) {
url := api.readURL(id)
msg := new(Message)
res, err := api.get(url, http.Header{}, msg)
// type assertion
msg, _ = res.(*Message)
return msg, err
}
restclient/restclient.go
package restclient
type restAPI struct {
readClient *resty.Client
}
func (api *restAPI) get(url string, h http.Header, v interface{}) (interface{}, error) {
...
r, err := req.Get(url)
// handling error and returns the response properly
return v, nil
}
Se utiliza la composición de tipos en go, componiendo el tipo restAPI
en msgAPI
. En el tipo msgAPI
se aprovecha de una implementación genérica de GET HTTP usando un cliente rest de resty
.
Y la testeabilidad??!
Venimos enfocados en organización, dependencia entre módulos, organización de capas, pero no hablamos aún de algo muy importante que es el testing.
Dada la estructura planteada y la independencia entre capa y capa, es muy sencillo realizar pruebas unitarias de cada uno de nuestros módulos sin depender de módulos externos.
Debajo muestro un ejemplo de cómo probar las funciones de la capa Use cases, en particular del archivo service.go
.
message/service_test.go
package message
type mockRepo struct{}
func (repo *mockRepo) Get(id string) (*Message, error) {
if id == "error" {
return nil, errors.New("Mocked error")
}
return &Message{Text: "TestMessage"}, nil
}
func NewInmemRepository() Repository {
return new(mockRepo)
}
func Test_messageSrv_Get(t *testing.T) {
// creates in memory message repository
repo := NewInmemRepository()
// create new service instance in order to test
srv := NewService(repo)
type args struct {
id string
}
tests := []struct {
name string
args args
want *Message
wantErr bool
}{
{
"test OK",
args{"myMessageID"},
&Message{Text: "TestMessage"},
false,
},
{
"test fail",
args{"error"},
nil,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := srv.Get(tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("messageSrv.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("messageSrv.Get() = %v, want %v", got, tt.want)
}
})
}
}
Como vemos en el ejemplo, en este caso es muy sencillo implementar un "mock" que nos permita probar la lógica de la función Get
sin depender de la lógica subyacente del repositorio (no dependemos si utilizamos una DB, una API, o cualquier otro framework).
En el mock implementado en el ejemplo simulamos con el string "error", un caso de error en el repositorio. De esa forma podemos plantear un caso de éxito y un caso de error muy fácilmente y sin depender de otros módulos.
TIP con esta herramienta en go pueden generar unos hermosos test siguiendo el patrón de Data Driven Testing (DDT) (o también conocido como Table Driven Testing o TDT)
Conclusión
Luego de leer tutoriales, hacer pruebas de concepto, de probar, de equivocarme, de intentar generar código idiomático en go, luego de haber leído decenas de artículos usando los principios de Uncle Bob y su famosa clean architecture, finalmente, este es mi humilde aporte a la comunidad (en español) de cómo estructurar tu aplicación en go.
Usando la estructura del ejemplo de la API de mensajes se puede extrapolar a cualquier otra aplicación. En resumen:
- api: en este caso nos da la interfaz con el usuario mediante una rest API, pero podría ser un command line u cualquier otro tipo de interfaz. El nombre del módulo cambiaría según el caso.
- message: en este módulo podemos encapsular nuestra lógica de negocio y casos de uso. El nombre del módulo refiere al domino de la aplicación por lo que podría ser cualquier cosa. Deben ser nombres en singular siguiendo naming conventions de go. Ejemplo: user, customer, item, etc.
- restclient: este es un simple ejemplo de un módulo externo para dar servicios a la capa de Use case. Otros ejemplos podrían ser, módulos con namings como mysqlhandler, dbhandler, elastichandler, dbutil, siendo todos ellos conectores a distintos gestores de datos.
En mi repositorio de GitHub dejo un ejemplo totalmente funcional que pueden utilizar libremente.
Happy structuring :D (creo que inventé la expresión...)
Referencias
- Go for newcomers - Excelente para los más nuevos en go
- Go By example - Una página con un enfoque MUY práctico llena de ejemplos de código y que es posible probar en el playground.
- What's in a name - Presentación sobre naming conventions en go
- Uncle Bob - Clean Architecture
- Clean Architecture Using Golang
- Applying The Clean Architecture to Go applications
Top comments (1)
Muy buen artículo!