Diseño Avanzado de Casos de Uso en Go: Datos, Reglas de Negocio y Mensajería
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:
Datos: Interacción directa con la base de datos (usando GORM-GEN u otros ORM).
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).
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ía | Descripción | Ejemplo |
usecase.Data | Acceso directo a la base de datos (queries y mutaciones) | usecase.Data .GetByID(userID) |
usecase.BusinessRules | Lógica empresarial sin acceso a BD directo | usecase.BusinessRules.CanUserBuy(userID) |
usecase.Messages | Manejo de eventos y comunicación | usecase.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.