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

·

4 min read

Este otro artículo [Convenciones de Nombres para Funciones Booleanas y de Manipulación de Datos en Go] ayuda a potencializar lo dicho aqui.

La creación de casos de uso bien estructurados en Go permite una arquitectura limpia, mantenible y escalable. Esta estructura se organiza en torno a tres categorías principales:

  1. Datos: Interacción directa con la base de datos (usando GORM-GEN u otros ORM).

  2. Reglas de Negocio: Lógica empresarial que no depende directamente de la base de datos (se reciben datos puros y se devuelven decisiones, se prohíbe el acceso a repositorios de datos o usar casos de uso).

  3. Mensajería/Eventos: Gestión de comunicación asincrónica y procesos impulsados por eventos.

🚀 Estructura de Casos de Uso Basada en lo anterior

Siguiendo esta idea, un caso de uso sería la composición de estos tres niveles:

  •       usecase.Data.GetByID(userID) // Obtiene los datos desde la BD (función de un ORM)
          usecase.BusinessRules.IsEligibleForUpgrade(user) // Aplica reglas de negocio
          usecase.Messages.SendUpgradeNotification(user) // Envía una notificación basada en la lógica
    

🔹 Categorías de Funciones en Casos de Uso

CategoríaDescripciónEjemplo
usecase.DataAcceso directo a la base de datos (queries y mutaciones)usecase.Data.GetByID(userID)
usecase.BusinessRulesLógica empresarial sin acceso a BD directousecase.BusinessRules.CanUserBuy(userID)
usecase.MessagesManejo de eventos y comunicaciónusecase.Messages.PublishOrderShipped(orderID)

Implementación de un Caso de Uso

Para implementar un caso de uso, combinamos estos tres componentes:

package usecase

import (
    "context"
    "fmt"
    "app/data"
    "app/business"
    "app/messages"
)

// UserPurchaseUseCase representa el caso de uso para manejar una compra de usuario.
type UserPurchaseUseCase struct {
    Data           *data.UserRepository
    BusinessRules  *business.UserRules
    Messages       *messages.NotificationService
}

// ProcessPurchase maneja la lógica completa de una compra de usuario.
func (uc *UserPurchaseUseCase) ProcessPurchase(ctx context.Context, userID, productID int) error {
    // Obtener datos del usuario.
    user, err := uc.Data.GetByID(ctx, userID)
    if err != nil {
        return err
    }

    // Aplicar reglas de negocio.
    if !uc.BusinessRules.CanUserBuy(user) {
        return fmt.Errorf("El usuario no es elegible para comprar")
    }

    // Enviar notificación de compra.
    uc.Messages.SendPurchaseConfirmation(user)
    return nil
}

Implementación de Reglas de Negocio

Las reglas de negocio se definen de forma que no dependen directamente de datos externos:

package business

import "app/models"

// UserRules contiene la lógica de negocio para usuarios.
type UserRules struct{}

// CanUserBuy evalúa si un usuario puede realizar una compra.
func (ur *UserRules) CanUserBuy(user *models.User) bool {
    return user.Active && user.Balance >= 0 && user.LastPurchaseDaysAgo >= 30
}

// IsUserEligibleForVIP verifica si el usuario califica para nivel VIP.
func (ur *UserRules) IsUserEligibleForVIP(user *models.User) bool {
    return user.PurchaseCount > 20 && user.LoyaltyPoints > 1000
}

Ejemplo Completo de Uso

A continuación, un ejemplo completo de cómo utilizar el caso de uso:

package main

import (
    "context"
    "fmt"
    "app/usecase"
)

func main() {
    ctx := context.Background()
    userPurchase := usecase.UserPurchaseUseCase{
        Data:          &usecase.Data,
        BusinessRules: &usecase.BusinessRules,
        Messages:      &usecase.Messages,
    }

    // Procesar la compra.
    err := userPurchase.ProcessPurchase(ctx, 1, 100)
    if err != nil {
        fmt.Println("Error en la compra:", err)
        return
    }
    fmt.Println("Compra realizada con éxito")

    // Consultar estado del usuario después de la compra.
    user, err := userPurchase.Data.GetByID(ctx, 1)
    if err != nil {
        fmt.Println("Error al consultar usuario después de la compra:", err)
        return
    }
    fmt.Printf("Estado del usuario después de la compra: %+v\n", user)

    // Verificar si el usuario es elegible para VIP.
    if userPurchase.BusinessRules.IsUserEligibleForVIP(user) {
        fmt.Println("El usuario ahora es VIP!")
        userPurchase.Messages.SendVIPWelcomeMessage(user)
    } else {
        fmt.Println("El usuario aún no es VIP")
    }
}

Ventajas de la Organización Propuesta

  • Separación de Responsabilidades: Los datos, reglas de negocio y mensajería están desacoplados, facilitando la mantenibilidad.

  • Facilidad para Testing: Cada capa puede ser testeada de forma independiente.

  • Flexibilidad y Escalabilidad: Permite la adición de nuevos casos de uso sin modificar la estructura existente.

  • Expresividad del Código: Al leer ProcessPurchase, la intención está clara sin detalles innecesarios.

Consideraciones

  • Reglas de negocio internas: Deben residir en el dominio si no dependen de datos externos.

  • Reglas que necesitan datos o eventos: Se definen en el caso de uso, integrando diferentes partes del sistema.

  • Reglas expuestas a otros servicios: Pueden convertirse en endpoints de API.

Conclusión

Este diseño proporciona un marco sólido y reutilizable para gestionar casos de uso en proyectos en Go, asegurando una separación clara de responsabilidades y permitiendo una evolución fluida del sistema.