Una regla que el framework no puede proteger
Una regla simple del dominio puede arruinar un sistema si vive en el lugar equivocado. En este artículo mostramos cómo una decisión de negocio acaba dispersa en controladores cuando el framework manda, y cómo la Arquitectura Hexagonal le da un hogar estable desde el inic
En el artículo anterior hablamos del dolor: sistemas que funcionan, pero que cada cambio vuelve más caro que el anterior. No por bugs evidentes, sino porque decisiones importantes quedaron atrapadas en lugares equivocados.
Este artículo muestra una de esas decisiones, usando un ejemplo real. No para teorizar sobre Arquitectura Hexagonal, sino para ver qué se rompe cuando no se toma en serio.
El problema concreto
En INGESTA existe una regla simple:
Un alumno no puede registrar tareas ni calificaciones de una materia si no está inscrito en ella.
Nada sofisticado.
Nada académico.
Nada que no hayas visto antes.
La pregunta importante no es qué hace la regla, sino dónde vive.
Enfoque común: framework primero
En un diseño típico, esta regla termina así:
[HttpPost("/tareas")]
public IActionResult CrearTarea(CrearTareaDto dto)
{
var inscripcion = _db.Inscripciones
.FirstOrDefault(i =>
i.AlumnoId == dto.AlumnoId &&
i.MateriaId == dto.MateriaId);
if (inscripcion == null)
return BadRequest("El alumno no está inscrito en la materia");
var tarea = new Tarea(dto.AlumnoId, dto.MateriaId, dto.Descripcion);
_db.Tareas.Add(tarea);
_db.SaveChanges();
return Ok();
}
Funciona.
Pasa QA.
Llega a producción.
Hasta que necesitas permitir tareas “pendientes” sin inscripción activa y descubres que esa misma regla está copiada, con pequeñas variaciones, en cinco endpoints distintos.
El sistema sigue funcionando.
El diseño ya no.
El problema real
La regla no es del controlador.
Tampoco es de la base de datos.
Y no es responsabilidad de la UI evitar violaciones de dominio.
Es una invariante de negocio que involucra varias entidades:
- Materia
- Inscripción
- Tarea
No hay un lugar natural para esta regla en un diseño framework-first. Por eso termina desparramada.
Aquí entra la Arquitectura Hexagonal
La Arquitectura Hexagonal no "mejora" este código.
Le da un lugar correcto a la decisión.
La regla vive en el núcleo y se expone a través de un puerto de entrada.
public interface IRegistrarTarea
{
void Ejecutar(RegistrarTareaCommand command);
}
El command que entra al caso de uso es explícito:
public sealed class RegistrarTareaCommand
{
public Guid AlumnoId { get; }
public Guid MateriaId { get; }
public string Descripcion { get; }
public RegistrarTareaCommand(
Guid alumnoId,
Guid materiaId,
string descripcion)
{
AlumnoId = alumnoId;
MateriaId = materiaId;
Descripcion = descripcion;
}
}
Implementación del caso de uso:
public sealed class RegistrarTareaUseCase : IRegistrarTarea
{
private readonly IInscripcionRepository inscripciones;
private readonly ITareaRepository tareas;
public RegistrarTareaUseCase(
IInscripcionRepository inscripciones,
ITareaRepository tareas)
{
this.inscripciones = inscripciones;
this.tareas = tareas;
}
public void Ejecutar(RegistrarTareaCommand command)
{
if (!inscripciones.Existe(command.AlumnoId, command.MateriaId))
throw new ReglaDeDominioViolada(
"No se puede registrar una tarea sin inscripción");
var tarea = Tarea.Crear(
command.AlumnoId,
command.MateriaId,
command.Descripcion);
tareas.Guardar(tarea);
}
}
Aquí la regla:
- existe una sola vez,
- no depende de HTTP,
- no depende de Entity Framework,
- no depende de cómo llegó la petición.
El framework solo adapta la llamada.
No decide nada.
La diferencia clave
En ambos enfoques el sistema funciona. La diferencia es quién controla la regla.
En el enfoque framework-first, la regla pertenece al flujo técnico. En Hexagonal, la regla pertenece al dominio y se invoca por contrato.
No es una cuestión de estilo. Es una cuestión de control.
El costo real
Este enfoque exige más trabajo inicial:
- más clases,
- más interfaces,
- más fricción al principio.
A cambio, obtienes algo concreto:
Una regla de negocio que no puede romperse por accidente.
Ese es el intercambio.
Siguiente paso
En el siguiente artículo nos vamos a detener solo en los casos de uso: qué son, qué no son y por qué son el primer límite real entre el núcleo y el mundo exterior.
Aquí ya no estamos hablando de Arquitectura Hexagonal.
La estamos usando para proteger algo que sí importa.
Nota
La idea de proteger invariantes de negocio mediante casos de uso explícitos está desarrollada en Domain-Driven Design de Eric Evans, particularmente en los capítulos dedicados al aislamiento del dominio y la coordinación de objetos del modelo.