Diseño Avanzado de Casos de Uso en Go: Datos, Reglas de Negocio y Mensajería Part 2

·

4 min read

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:

  1. Entidad (domain): Representa el modelo de negocio con validaciones y métodos de actualización.

  2. Reglas de negocio (business): Actúa como un envoltorio (wrapper) de las reglas definidas en la entidad.

  3. Repositorio (data): Maneja la persistencia de los datos en la base de datos.

  4. 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.