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, pero con una estructura un poco diferente. No muy diferente, el mismo concepto pero diferente en la estructura de carpetas. Kurio - App Berita Indonesia , Puede buscar un proyecto de muestra aquí , un artículo de administración de CRUD de muestra. https://github.com/bxcodec/go-clean-arch 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: 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. Comprobable. Las reglas comerciales se pueden probar sin la interfaz de usuario, la base de datos, el servidor web o cualquier otro elemento externo. 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. 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. 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: type Article struct { ID int64 Title string Content string UpdatedAt time.Time CreatedAt time.Time } import "time" `json:"id"` `json:"title"` `json:"content"` `json:"updated_at"` `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 models 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) } import "github.com/bxcodec/go-clean-arch/article" La capa de Usecase se comunicará con el Repositorio usando este contrato, y la capa de Repositorio implementar esta interfaz para que Usecase pueda usarla. DEBE Ejemplo de interfaz de Usecase package usecase ( ) 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) } import "github.com/bxcodec/go-clean-arch/article" Lo mismo con Usecase, la capa de entrega utilizará esta interfaz de contrato. Y la capa Usecase implementar esta interfaz. DEBE Prueba de cada capa Como sabemos, limpio significa independiente. Cada capa comprobable, incluso otras capas aún no existen. 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. Capa de modelos: 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. Repositorio: 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. Caso de uso: 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 Entrega: 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, := sqlmock.New() 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( , “title ”, “Content ”, 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( ) anArticle, := a.GetByID(num) assert.NoError(t, err) assert.NotNil(t, anArticle) } err if 1 1 1 1 err Prueba de caso de uso Prueba de muestra para la capa Usecase, que depende de la capa Repository. package usecase_test ( models ucase ) func TestFetch(t *testing.T) { := (mocks.ArticleRepository) mockArticle models.Article err := faker.FakeData(&mockArticle) assert.NoError(t, err) mockListArtilce := make([]*models.Article, ) mockListArtilce = append(mockListArtilce, &mockArticle) mockArticleRepo.On( , mock.AnythingOfType( ), mock.AnythingOfType( )).Return(mockListArtilce, nil) u := ucase.NewArticleUsecase(mockArticleRepo) num := int64( ) cursor := list, nextCursor, := 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, , mock.AnythingOfType( ), mock.AnythingOfType( )) } import "errors" "strconv" "testing" "github.com/bxcodec/faker" "github.com/bxcodec/go-clean-arch/article" "github.com/bxcodec/go-clean-arch/article/repository/mocks" "github.com/bxcodec/go-clean-arch/article/usecase" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" mockArticleRepo new var 0 "Fetch" "string" "int64" 1 "12" err "Fetch" "string" "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) { mockArticle models.Article err := faker.FakeData(&mockArticle) assert.NoError(t, err) mockUCase := (mocks.ArticleUsecase) num := int(mockArticle.ID) mockUCase.On(“GetByID”, int64(num)).Return(&mockArticle, nil) e := echo.New() req, := 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{ : mockUCase, : httpHelper.HttpHelper{} } handler.GetByID(c) assert.Equal(t, http.StatusOK, rec.Code) mockUCase.AssertCalled(t, “GetByID”, int64(num)) } var new err AUsecase Helper 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 ( httpDeliver articleRepo articleUcase cfg _ ) config cfg.Config func init() { config = cfg.NewViperConfig() config.GetBool( ) { fmt.Println( ) } } func main() { := config.GetString( ) dbPort := config.GetString( ) dbUser := config.GetString( ) dbPass := config.GetString( ) dbName := config.GetString( ) connection := fmt.Sprintf( , dbUser, dbPass, dbHost, dbPort, dbName) val := url.Values{} val.Add( , ) val.Add( , ) dsn := fmt.Sprintf( , connection, val.Encode()) dbConn, := sql.Open( , dsn) err != nil && config.GetBool( ) { 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( )) } import "database/sql" "fmt" "net/url" "github.com/bxcodec/go-clean-arch/article/delivery/http" "github.com/bxcodec/go-clean-arch/article/repository/mysql" "github.com/bxcodec/go-clean-arch/article/usecase" "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 if `debug` "Service RUN on DEBUG mode" dbHost `database.host` `database.port` `database.user` `database.pass` `database.name` "%s:%s@tcp(%s:%s)/%s" "parseTime" "1" "loc" "Asia/Jakarta" "%s?%s" err `mysql` if "debug" "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 enviarme un Si tienes alguna duda, o necesitas más explicación, o algo, que no puedo explicar bien aquí, puedes preguntarme desde mi linkin o correo electrónico . Gracias