Resumen
Este artículo propone una solución utilizando Clean Architecture para manejar múltiples fuentes de datos con estructuras diversas, unificándolas en un modelo de dominio común llamado Customer. La solución se basa en la implementación de diferentes repositorios para fuentes de datos como REST APIs y bases de datos SQL (MySQL-DB1, MySQL-DB2). Estos repositorios transforman los datos de cada fuente a un modelo estandarizado. Además, se usa una Factory que selecciona dinámicamente la implementación adecuada del repositorio, manteniendo la lógica de negocio desacoplada de los detalles de infraestructura.
Introducción
En sistemas complejos, a menudo los datos provienen de múltiples fuentes con estructuras diferentes. Este problema puede resolverse mediante la Clean Architecture, desacoplando la lógica de negocio de los detalles de infraestructura. En este paper, implementamos una solución que permite trabajar con diferentes fuentes de datos para la entidad Customer. Estas fuentes incluyen una API REST y dos bases de datos MySQL con estructuras diferentes, pero todas se unifican en un modelo de dominio común.
El uso de una Factory asegura que la lógica de negocio se mantenga independiente de la fuente de datos, seleccionando dinámicamente el repositorio correcto.
Árbol de Archivos y Carpetas
src/
│
├── domain/
│ ├── models/
│ │ └── Customer.go // Modelo de dominio para Customer
│ └── repositories/
│ └── CustomerRepository.go // Interfaz común para todas las fuentes de datos
│
├── infrastructure/
│ ├── repositories/
│ │ └── customer/
│ │ ├── rest/
│ │ │ └── CustomerRestRepository.go // Implementación para API REST
│ │ ├── mysql-db1/
│ │ │ └── CustomerMySQLDB1Repository.go // Implementación para MySQL DB1
│ │ ├── mysql-db2/
│ │ │ └── CustomerMySQLDB2Repository.go // Implementación para MySQL DB2
│ └── factory/
│ └── CustomerRepositoryFactory.go // Selección de la implementación según la fuente de datos
│
├── application/
│ └── usecases/
│ └── GetCustomerById.go // Caso de uso para obtener un Customer por ID
│
└── main.go // Punto de entrada donde se usa la Factory y el Caso de Uso
Modelo de Dominio: Customer
// src/domain/models/Customer.go
package models
type Customer struct {
ID string
Name string
Email string
}
El modelo de dominio Customer representa la estructura estándar con la que deben trabajar todas las implementaciones de los repositorios, independientemente de su fuente de datos.
Interfaz del Repositorio
// src/domain/repositories/CustomerRepository.go
package repositories
import "src/domain/models"
type CustomerRepository interface {
GetCustomerById(id string) (*models.Customer, error)
}
La interfaz CustomerRepository define el contrato que todas las fuentes de datos deben cumplir. Cada implementación transformará los datos específicos de su fuente a la estructura Customer.
Implementaciones del Repositorio
Cada repositorio se encarga de transformar los datos de su fuente a la estructura Customer.
Repositorio para API REST
// src/infrastructure/repositories/customer/rest/CustomerRestRepository.go
package rest
import (
"encoding/json"
"fmt"
"net/http"
"src/domain/models"
"src/domain/repositories"
)
type CustomerRestRepository struct{}
func NewCustomerRestRepository() repositories.CustomerRepository {
return &CustomerRestRepository{}
}
func (r *CustomerRestRepository) GetCustomerById(id string) (*models.Customer, error) {
url := fmt.Sprintf("https://api.example.com/customer/%s", id)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var restCustomer struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(resp.Body).Decode(&restCustomer); err != nil {
return nil, err
}
customer := &models.Customer{
ID: restCustomer.ID,
Name: restCustomer.Name,
Email: restCustomer.Email,
}
return customer, nil
}
Repositorio para MySQL DB1
// src/infrastructure/repositories/customer/mysql-db1/CustomerMySQLDB1Repository.go
package mysqldb1
import (
"src/domain/models"
"src/domain/repositories"
)
type CustomerMySQLDB1Repository struct{}
func NewCustomerMySQLDB1Repository() repositories.CustomerRepository {
return &CustomerMySQLDB1Repository{}
}
func (r *CustomerMySQLDB1Repository) GetCustomerById(id string) (*models.Customer, error) {
var customer models.Customer
// Supongamos que DB1 tiene los campos ID, full_name y correo_electronico
customer = models.Customer{
ID: id,
Name: "Full Name from DB1",
Email: "email_db1@example.com",
}
return &customer, nil
}
Repositorio para MySQL DB2
// src/infrastructure/repositories/customer/mysql-db2/CustomerMySQLDB2Repository.go
package mysqldb2
import (
"src/domain/models"
"src/domain/repositories"
)
type CustomerMySQLDB2Repository struct{}
func NewCustomerMySQLDB2Repository() repositories.CustomerRepository {
return &CustomerMySQLDB2Repository{}
}
func (r *CustomerMySQLDB2Repository) GetCustomerById(id string) (*models.Customer, error) {
var customer models.Customer
// Supongamos que DB2 tiene los campos customer_id, name y email_address
customer = models.Customer{
ID: id,
Name: "Full Name from DB2",
Email: "email_db2@example.com",
}
return &customer, nil
}
Factory: Selección del Repositorio
La Factory selecciona la implementación adecuada según la variable de entorno CUSTOMER_DATA_SOURCE. Esto permite cambiar entre fuentes de datos sin alterar la lógica de negocio.
// src/infrastructure/factory/CustomerRepositoryFactory.go
package factory
import (
"os"
"src/infrastructure/repositories/customer/mysqldb1"
"src/infrastructure/repositories/customer/mysqldb2"
"src/infrastructure/repositories/customer/rest"
"src/domain/repositories"
"fmt"
)
func CreateCustomerRepository() (repositories.CustomerRepository, error) {
source := os.Getenv("CUSTOMER_DATA_SOURCE")
switch source {
case "rest":
return rest.NewCustomerRestRepository(), nil
case "mysql-db1":
return mysqldb1.NewCustomerMySQLDB1Repository(), nil
case "mysql-db2":
return mysqldb2.NewCustomerMySQLDB2Repository(), nil
default:
return nil, fmt.Errorf("invalid data source")
}
}
Caso de Uso: Obtener un Customer por ID
El caso de uso interactúa con la interfaz CustomerRepository y no sabe ni le importa de dónde provienen los datos. Esto garantiza un desacoplamiento total.
src/application/usecases/GetCustomerById.go
package usecases
import (
"src/domain/models"
"src/domain/repositories"
)
func GetCustomerById(repo repositories.CustomerRepository, id string) (*models.Customer, error) {
customer, err := repo.GetCustomerById(id)
if err != nil {
return nil, err
}
return customer, nil
}
Ejecución del Caso de Uso
En el archivo main.go, la Factory selecciona el repositorio adecuado basándose en la variable de entorno CUSTOMER_DATA_SOURCE. Luego, el caso de uso se ejecuta de forma agnóstica respecto a la fuente de datos.
// src/main.go
package main
import (
"fmt"
"os"
"src/application/usecases"
"src/infrastructure/factory"
)
func main() {
// Definir CUSTOMER_DATA_SOURCE directamente para pruebas
os.Setenv("CUSTOMER_DATA_SOURCE", "rest") // O "mysql-db1", "mysql-db2"
customerID := "12345"
repo, err := factory.CreateCustomerRepository()
if err != nil {
fmt.Printf("Error creating repository: %s\n", err)
os.Exit(1)
}
customer, err := usecases.GetCustomerById(repo, customerID)
if err != nil {
fmt.Printf("Error fetching customer: %s\n", err)
os.Exit(1)
}
fmt.Printf("Customer: %+v\n", customer)
}
Conclusión
Este enfoque implementa una solución altamente flexible y desacoplada, que permite obtener datos desde múltiples fuentes con estructuras diferentes, pero unificando la salida en un modelo de dominio común