Diseño Avanzado de Casos de Uso en Go: Datos, Reglas de Negocio y Mensajería Part 2
Introducción
Este documento detalla una arquitectura limpia para el manejo de casos de uso en Go, utilizando un enfoque modular y escalable. Se presenta una separación clara de responsabilidades en:
Entidad (
domain
): Representa el modelo de negocio con validaciones y métodos de actualización.Reglas de negocio (
business
): Actúa como un envoltorio (wrapper) de las reglas definidas en la entidad.Repositorio (
data
): Maneja la persistencia de los datos en la base de datos.Caso de uso (
usecase
): Expone las reglas y el acceso a datos de manera uniforme.
Este diseño permite un código mantenible, fácil de extender y óptimo para la generación automática de código en proyectos grandes.
1️⃣ domain/user.go
- Definición de la Entidad
package domain
import (
"errors"
"strings"
)
// User representa a un usuario con datos y reglas de negocio
type User struct {
ID string
Name string
Email string
Balance float64
Active bool
Age int
}
// ------------------------ MÉTODOS DE VALIDACIÓN ------------------------
// Verifica si el usuario tiene suficiente saldo para comprar
func (u *User) CanUserBuy(amount float64) bool {
return u.Balance >= amount
}
// Verifica si el usuario es elegible para vender
func (u *User) IsUserEligibleForSale() bool {
return u.Active && u.Age >= 18
}
// ------------------------ MÉTODOS DE ACTUALIZACIÓN ------------------------
// Cambia el email del usuario con validación básica
func (u *User) UpdateEmail(newEmail string) error {
if !strings.Contains(newEmail, "@") {
return errors.New("email inválido")
}
u.Email = newEmail
return nil
}
// Activa la cuenta del usuario
func (u *User) Activate() {
u.Active = true
}
// Desactiva la cuenta del usuario
func (u *User) Deactivate() {
u.Active = false
}
// Cambia la edad del usuario
func (u *User) SetAge(newAge int) error {
if newAge < 0 {
return errors.New("la edad no puede ser negativa")
}
u.Age = newAge
return nil
}
// Cambia el nombre del usuario
func (u *User) SetName(newName string) error {
if len(strings.TrimSpace(newName)) == 0 {
return errors.New("el nombre no puede estar vacío")
}
u.Name = newName
return nil
}
2️⃣ business/user_rules.go
- Reglas de Negocio (Wrapper)
package business
import "myapp/domain"
// UserRules sirve como wrapper para exponer reglas de negocio
type UserRules struct{}
// Mantiene los mismos nombres de las funciones de la entidad
func (ur *UserRules) CanUserBuy(user *domain.User, amount float64) bool {
return user.CanUserBuy(amount)
}
func (ur *UserRules) IsUserEligibleForSale(user *domain.User) bool {
return user.IsUserEligibleForSale()
}
3️⃣ data/user_repository.go
- Acceso a Datos (ORM)
package data
import (
"context"
"database/sql"
"myapp/domain"
)
// UserRepository maneja la persistencia de usuarios
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
// Obtiene un usuario por ID desde la BD
func (r *UserRepository) GetByID(ctx context.Context, id string) (*domain.User, error) {
var user domain.User
err := r.db.QueryRowContext(ctx, "SELECT id, name, email, balance, active, age FROM users WHERE id = ?", id).
Scan(&user.ID, &user.Name, &user.Email, &user.Balance, &user.Active, &user.Age)
if err != nil {
return nil, err
}
return &user, nil
}
// Guarda el usuario en la BD
func (r *UserRepository) Save(ctx context.Context, user *domain.User) error {
_, err := r.db.ExecContext(ctx, "UPDATE users SET name=?, email=?, balance=?, active=?, age=? WHERE id=?",
user.Name, user.Email, user.Balance, user.Active, user.Age, user.ID)
return err
}
5️⃣ main.go
- Ejemplo de Uso
package main
import (
"context"
"fmt"
"myapp/usecase"
"myapp/data"
"myapp/business"
)
func main() {
ctx := context.Background()
userRepo := &data.UserRepository{}
userRules := &business.UserRules{}
userUC := usecase.UserUsecase{
Data: userRepo,
Rules: userRules,
}
// 🔹 Acceder a métodos del ORM desde el caso de uso sin que parezca sucio
user, err := userUC.Data.GetByID(ctx, "123")
if err != nil {
fmt.Println("Error al obtener usuario:", err)
} else {
fmt.Println("Usuario obtenido:", user)
}
// 🔹 Acceder a reglas de negocio desde el caso de uso
if userUC.Rules.IsUserEligibleForSale(user) {
fmt.Println("El usuario puede vender")
}
// 🔹 Usar el caso de uso normalmente para ejecutar lógica de aplicación
err = userUC.ProcessPurchase(ctx, "123", 100)
if err != nil {
fmt.Println("Error en la compra:", err)
} else {
fmt.Println("Compra realizada con éxito")
}
}
Conclusión
Este diseño proporciona una arquitectura limpia, modular y escalable en Go, manteniendo una separación clara entre:
Entidad (
domain
) con validaciones y actualizaciones.Reglas de negocio (
business
) como un wrapper de la entidad.Persistencia (
data
) para la interacción con la base de datos.Casos de uso (
usecase
) como punto de acceso a reglas y datos.
🚀 Ideal para generación automática de código y sistemas escalables.