paint-brush
Probando la arquitectura limpia en Golangpor@imantumorang
103,561 lecturas
103,561 lecturas

Probando la arquitectura limpia en Golang

por Iman Tumorang7m2017/06/06
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

Después de leer el concepto de arquitectura limpia del tío Bob, estoy tratando de implementarlo en Golang. Esta es una arquitectura similar a la que usamos en nuestra empresa, Kurio - App Berita Indonesia, pero con una estructura un poco diferente. La arquitectura no depende de la existencia de alguna biblioteca de software cargado de funciones. Las reglas comerciales se pueden probar sin la interfaz de usuario, la base de datos, el servidor web o cualquier otro elemento externo. Puedes sustituir cualquier cosa aquí, por una propia o de un tercero que tenga las mismas funciones.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Probando la arquitectura limpia en Golang
Iman Tumorang HackerNoon profile picture

Independiente, comprobable y limpio

Después de leer el concepto de arquitectura limpia del tío Bob, estoy tratando de implementarlo en Golang. Esta es una arquitectura similar a la que usamos en nuestra empresa, Kurio - App Berita Indonesia , pero con una estructura un poco diferente. No muy diferente, el mismo concepto pero diferente en la estructura de carpetas.

Puede buscar un proyecto de muestra aquí https://github.com/bxcodec/go-clean-arch , un artículo de administración de CRUD de muestra.

Descargo de responsabilidad :
No estoy recomendando ninguna biblioteca o marco utilizado aquí. Puedes sustituir cualquier cosa aquí, por una propia o de un tercero que tenga las mismas funciones.

Básico

Como sabemos, las restricciones antes de diseñar la Arquitectura Limpia son:

  1. Independiente de Frameworks. La arquitectura no depende de la existencia de alguna biblioteca de software cargado de funciones. Esto le permite utilizar dichos marcos como herramientas, en lugar de tener que abarrotar su sistema con sus limitaciones limitadas.
  2. Comprobable. Las reglas comerciales se pueden probar sin la interfaz de usuario, la base de datos, el servidor web o cualquier otro elemento externo.
  3. Independiente de la interfaz de usuario. La interfaz de usuario puede cambiar fácilmente, sin cambiar el resto del sistema. Una interfaz de usuario web podría reemplazarse con una interfaz de usuario de consola, por ejemplo, sin cambiar las reglas comerciales.
  4. Independiente de la base de datos. Puede cambiar Oracle o SQL Server por Mongo, BigTable, CouchDB o cualquier otra cosa. Sus reglas comerciales no están vinculadas a la base de datos.
  5. Independiente de cualquier agencia externa. De hecho, las reglas de su negocio simplemente no saben nada sobre el mundo exterior. Más información en https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

Entonces, según esta restricción, cada capa debe ser independiente y comprobable.

Si la arquitectura del tío Bob tiene 4 capas:

  • Entidades
  • caso de uso
  • Controlador
  • Marco y controlador

En mis proyectos, también estoy usando 4:

  • Modelos
  • Repositorio
  • caso de uso
  • Entrega

Modelos

Igual que las Entidades, se usará en todas las capas. Esta capa almacenará la estructura de cualquier objeto y su método. Ejemplo: Artículo, Estudiante, Libro.
Estructura de ejemplo:

 import "time" type Article struct { ID int64 `json:"id"` Title string `json:"title"` Content string `json:"content"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` }

Cualquier entidad o modelo se almacenará aquí.

Repositorio

El repositorio almacenará cualquier controlador de base de datos. Las consultas, o la creación/inserción en cualquier base de datos se almacenarán aquí. Esta capa actuará solo para CRUD en la base de datos. Aquí no ocurre ningún proceso de negocio. Solo función simple para la base de datos.

Esta capa también tiene la responsabilidad de elegir qué base de datos se utilizará en la aplicación. Podría ser Mysql, MongoDB, MariaDB, Postgresql lo que sea, se decidirá aquí.

Si usa ORM, esta capa controlará la entrada y la entregará directamente a los servicios ORM.

Si llama a microservicios, se manejará aquí. Cree una solicitud HTTP a otros servicios y desinfecte los datos. Esta capa, debe actuar completamente como un repositorio. Manejar toda la entrada de datos: la salida no sucede ninguna lógica específica.

Esta capa de Repositorio dependerá de Connected DB u otros microservicios, si existen.

caso de uso

Esta capa actuará como el controlador de procesos comerciales. Cualquier proceso será manejado aquí. Esta capa decidirá qué capa de repositorio utilizará. Y tiene la responsabilidad de proporcionar datos para servir en la entrega. Procese los datos haciendo cálculos o cualquier cosa se hará aquí.

La capa de caso de uso aceptará cualquier entrada de la capa de entrega, que ya esté desinfectada, luego procesará la entrada que podría almacenarse en la base de datos, o recuperarla de la base de datos, etc.

Esta capa de caso de uso dependerá de la capa de repositorio

Entrega

Esta capa actuará como presentador. Decide cómo se presentarán los datos. Podría ser una API REST, un archivo HTML o gRPC sea cual sea el tipo de entrega.
Esta capa también aceptará la entrada del usuario. Desinfecte la entrada y envíela a la capa Usecase.

Para mi proyecto de muestra, estoy usando REST API como método de entrega.
El cliente llamará al extremo del recurso a través de la red y la capa de entrega obtendrá la entrada o solicitud y la enviará a la capa de casos de uso.

Esta capa dependerá de la capa de casos de uso.

Comunicaciones entre capas

Excepto modelos, cada capa se comunicará a través de la interfaz. Por ejemplo, la capa de casos de uso necesita la capa de repositorio, entonces, ¿cómo se comunican? El repositorio proporcionará una interfaz para ser su contrato y comunicación.

Ejemplo de la interfaz del repositorio

 package repository import models "github.com/bxcodec/go-clean-arch/article" type ArticleRepository interface { Fetch(cursor string, num int64) ([]*models.Article, error) GetByID(id int64) (*models.Article, error) GetByTitle(title string) (*models.Article, error) Update(article *models.Article) (*models.Article, error) Store(a *models.Article) (int64, error) Delete(id int64) (bool, error) }


La capa de Usecase se comunicará con el Repositorio usando este contrato, y la capa de Repositorio DEBE implementar esta interfaz para que Usecase pueda usarla.

Ejemplo de interfaz de Usecase

 package usecase import ( "github.com/bxcodec/go-clean-arch/article" ) type ArticleUsecase interface { Fetch(cursor string, num int64) ([]*article.Article, string, error) GetByID(id int64) (*article.Article, error) Update(ar *article.Article) (*article.Article, error) GetByTitle(title string) (*article.Article, error) Store(*article.Article) (*article.Article, error) Delete(id int64) (bool, error) }


Lo mismo con Usecase, la capa de entrega utilizará esta interfaz de contrato. Y la capa Usecase DEBE implementar esta interfaz.

Prueba de cada capa

Como sabemos, limpio significa independiente. Cada capa comprobable, incluso otras capas aún no existen.

  • Capa de modelos: esta capa solo prueba si se declara alguna función/método en alguna de las estructuras. Y puede probar fácilmente e independientemente de otras capas.
  • Repositorio: para probar esta capa, la mejor manera es hacer pruebas de Integraciones. Pero también puedes burlarte de cada prueba. Estoy usando github.com/DATA-DOG/go-sqlmock como mi ayudante para simular el proceso de consulta msyql.
  • Caso de uso: Debido a que esta capa depende de la capa de Repositorio, significa que esta capa necesita la capa de Repositorio para la prueba. Entonces debemos hacer una maqueta del Repositorio que se burló con burla, en base a la interfaz de contrato definida anteriormente.
  • Entrega: Lo mismo con Usecase, porque esta capa depende de la capa Usecase, lo que significa que necesitamos la capa Usecase para las pruebas. Y la capa Usecase también debe burlarse con burla, según la interfaz de contrato definida antes

Para burlarme, uso la burla para golang de vektra que se puede ver aquí https://github.com/vektra/mockery

Prueba de repositorio

Para probar esta capa, como dije antes, estoy usando un sql-mock para simular mi proceso de consulta. Puede usar como lo que usé aquí github.com/DATA-DOG/go-sqlmock u otro que tenga una función similar

 func TestGetByID(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf(“an error '%s' was not expected when opening a stub database connection”, err) } defer db.Close() rows := sqlmock.NewRows([]string{ “id”, “title”, “content”, “updated_at”, “created_at”}). AddRow( 1 , “title 1 ”, “Content 1 ”, time.Now(), time.Now()) query := “SELECT id,title,content,updated_at, created_at FROM article WHERE ID = \\?” mock.ExpectQuery(query).WillReturnRows(rows) a := articleRepo.NewMysqlArticleRepository(db) num := int64( 1 ) anArticle, err := a.GetByID(num) assert.NoError(t, err) assert.NotNil(t, anArticle) }

Prueba de caso de uso

Prueba de muestra para la capa Usecase, que depende de la capa Repository.

 package usecase_test import ( "errors" "strconv" "testing" "github.com/bxcodec/faker" models "github.com/bxcodec/go-clean-arch/article" "github.com/bxcodec/go-clean-arch/article/repository/mocks" ucase "github.com/bxcodec/go-clean-arch/article/usecase" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestFetch(t *testing.T) { mockArticleRepo := new (mocks.ArticleRepository) var mockArticle models.Article err := faker.FakeData(&mockArticle) assert.NoError(t, err) mockListArtilce := make([]*models.Article, 0 ) mockListArtilce = append(mockListArtilce, &mockArticle) mockArticleRepo.On( "Fetch" , mock.AnythingOfType( "string" ), mock.AnythingOfType( "int64" )).Return(mockListArtilce, nil) u := ucase.NewArticleUsecase(mockArticleRepo) num := int64( 1 ) cursor := "12" list, nextCursor, err := u.Fetch(cursor, num) cursorExpected := strconv.Itoa(int(mockArticle.ID)) assert.Equal(t, cursorExpected, nextCursor) assert.NotEmpty(t, nextCursor) assert.NoError(t, err) assert.Len(t, list, len(mockListArtilce)) mockArticleRepo.AssertCalled(t, "Fetch" , mock.AnythingOfType( "string" ), mock.AnythingOfType( "int64" )) }


Mockery generará una maqueta de la capa del repositorio para mí. Así que no necesito terminar mi capa de Repositorio primero. Puedo trabajar terminando mi caso de uso primero, incluso si mi capa de repositorio aún no está implementada.

Prueba de entrega

La prueba de entrega dependerá de cómo entregue los datos. Si usa la API REST de http, podemos usar httptest, un paquete integrado para httptest en golang.

Debido a que depende de Usecase, necesitamos una simulación de Usecase. Lo mismo con Repository, también estoy usando Mockery para simular mi caso de uso, para pruebas de entrega.

 func TestGetByID(t *testing.T) { var mockArticle models.Article err := faker.FakeData(&mockArticle) assert.NoError(t, err) mockUCase := new (mocks.ArticleUsecase) num := int(mockArticle.ID) mockUCase.On(“GetByID”, int64(num)).Return(&mockArticle, nil) e := echo.New() req, err := http.NewRequest(echo.GET, “/article/” + strconv.Itoa(int(num)), strings.NewReader(“”)) assert.NoError(t, err) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetPath(“article/:id”) c.SetParamNames(“id”) c.SetParamValues(strconv.Itoa(num)) handler:= articleHttp.ArticleHandler{ AUsecase : mockUCase, Helper : httpHelper.HttpHelper{} } handler.GetByID(c) assert.Equal(t, http.StatusOK, rec.Code) mockUCase.AssertCalled(t, “GetByID”, int64(num)) }


Salida final y la fusión

Después de terminar toda la capa y ya pasó las pruebas. Debe fusionarse en un sistema en main.go en el proyecto raíz.
Aquí definirá y creará todas las necesidades del entorno y fusionará todas las capas en una sola.

Busque mi main.go como ejemplo:

 package main import ( "database/sql" "fmt" "net/url" httpDeliver "github.com/bxcodec/go-clean-arch/article/delivery/http" articleRepo "github.com/bxcodec/go-clean-arch/article/repository/mysql" articleUcase "github.com/bxcodec/go-clean-arch/article/usecase" cfg "github.com/bxcodec/go-clean-arch/config/env" "github.com/bxcodec/go-clean-arch/config/middleware" _ "github.com/go-sql-driver/mysql" "github.com/labstack/echo" ) var config cfg.Config func init() { config = cfg.NewViperConfig() if config.GetBool( `debug` ) { fmt.Println( "Service RUN on DEBUG mode" ) } } func main() { dbHost := config.GetString( `database.host` ) dbPort := config.GetString( `database.port` ) dbUser := config.GetString( `database.user` ) dbPass := config.GetString( `database.pass` ) dbName := config.GetString( `database.name` ) connection := fmt.Sprintf( "%s:%s@tcp(%s:%s)/%s" , dbUser, dbPass, dbHost, dbPort, dbName) val := url.Values{} val.Add( "parseTime" , "1" ) val.Add( "loc" , "Asia/Jakarta" ) dsn := fmt.Sprintf( "%s?%s" , connection, val.Encode()) dbConn, err := sql.Open( `mysql` , dsn) if err != nil && config.GetBool( "debug" ) { fmt.Println(err) } defer dbConn.Close() e := echo.New() middL := middleware.InitMiddleware() e.Use(middL.CORS) ar := articleRepo.NewMysqlArticleRepository(dbConn) au := articleUcase.NewArticleUsecase(ar) httpDeliver.NewArticleHttpHandler(e, au) e.Start(config.GetString( "server.address" )) }


Puede ver, cada capa se fusiona en una con sus dependencias.

Conclusión

  • En resumen, si se dibuja en un diagrama, se puede ver a continuación

  • Cada biblioteca utilizada aquí puede cambiar por su cuenta. Porque el punto principal de la arquitectura limpia es: no importa su biblioteca, pero su arquitectura es limpia y comprobable también independiente
  • Así es como organicé mis proyectos, puedes discutir, o estar de acuerdo, o tal vez mejorar esto para mejorar, solo deja un comentario y comparte esto

Los proyectos de muestra

El proyecto de muestra se puede ver aquí https://github.com/bxcodec/go-clean-arch

Biblioteca utilizada para mi proyecto:

  • Glide: para la gestión de paquetes
  • ir-sqlmock de github.com/DATA-DOG/go-sqlmock
  • Testificar: para probar
  • Echo Labstack (Golang Web Framework) para la capa de entrega
  • Viper: para configuraciones de entorno

Lecturas adicionales sobre arquitectura limpia:

  • Segunda parte de este artículo: https://hackernoon.com/trying-clean-architecture-on-golang-2-44d615bf8fdf
  • https://8thlight.com/blog/uncle-bob/2012/08/13/la-arquitectura-limpia.html
  • http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/ . Otra versión de Arquitectura Limpia en Golang

Si tienes alguna duda, o necesitas más explicación, o algo, que no puedo explicar bien aquí, puedes preguntarme desde mi linkin o enviarme un correo electrónico . Gracias