La Arquitectura Hexagonal

La Arquitectura Hexagonal
Photo by Brianna Marble / Unsplash

Contratos, Interfaces, Repositorios, Servicios y Value Objects

Este documento explica los conceptos fundamentales de Arquitectura Hexagonal (Ports & Adapters) aplicados al proyecto ControlEscolar, una aplicación personal para que un alumno de la UnADM gestione sus materias, inscripciones y calificaciones utilizando buenas prácticas de ingeniería de software modernas (DDD + .NET 10 LTS).

El objetivo es enseñar, de manera clara y práctica, cómo separar la lógica del dominio de los detalles técnicos como bases de datos, ORMs, APIs o interfaces de usuario.


1. ¿Qué es un Value Object?

En Domain-Driven Design un Value Object (VO) es un tipo del dominio cuyo valor está determinado únicamente por sus atributos, no por identidad.

Ejemplos típicos en software:

  • Dinero (cantidad + moneda)
  • Coordenada (latitud + longitud)
  • Email
  • Intervalo de fechas

En ControlEscolar son Value Objects:

  • CodigoMateria
  • PeriodoLectivo
  • (Opcionalmente) Calificacion

Propiedades de un Value Object

  1. Inmutables
    Una vez creados no se modifican.

  2. Siempre válidos
    El constructor garantiza que el estado interno es correcto.

  3. Igualdad estructural
    Dos VOs son iguales si sus valores son iguales.

  4. No tienen identidad
    No necesitan ID ni se rastrean como entidades propias.

¿Por qué usarlos?

  • Evitan duplicar validaciones.
  • Aumentan la claridad del dominio.
  • Previenen estados inconsistentes.
  • Facilitan las pruebas unitarias.

En ControlEscolar, CodigoMateria valida y expresa la estructura oficial de una materia; PeriodoLectivo asegura que un periodo siempre tenga un año válido y una parte 1 o 2.


2. ¿Qué son los Contratos en Arquitectura Hexagonal?

En Arquitectura Hexagonal, un Contrato es una interfaz definida dentro del Dominio que describe qué necesita el dominio del mundo exterior, sin conocer detalles técnicos.

El dominio NO debe depender de:

  • EF Core
  • SQL Server
  • APIs externas
  • Archivos
  • Frameworks

Por eso define interfaces, no implementaciones.

Un Contrato es entonces:

Una promesa sobre el comportamiento esperado, sin definir cómo se llevará a cabo.

Ejemplo de contrato en ControlEscolar:

public interface IMateriaRepository {
    Task<Materia?> ObtenerPorIdAsync(Guid id);
    Task<Materia?> ObtenerPorCodigoAsync(CodigoMateria codigo);
    Task<List<Materia>> ListarTodasAsync();
}

El dominio dice “necesito estas operaciones”, pero no sabe cómo se implementan.


3. Interfaces (Ports)

Una interfaz en el dominio es la expresión concreta del contrato.
En Hexagonal, estas interfaces son los puertos por los que el dominio se comunica con la infraestructura.

Características

  • Se definen en el Dominio.
  • No contienen código técnico.
  • No conocen SQL, EF Core ni HTTP.
  • Son consumidas por los casos de uso (Application layer).

¿Por qué son necesarias?

Porque permiten que:

  • El dominio sea agnóstico a la tecnología.
  • La aplicación pueda tener múltiples implementaciones (SQL Server, pruebas en memoria, mocks).
  • El sistema sea testeable sin levantar una base de datos.
  • Puedas cambiar de tecnología sin reescribir el dominio.

Sin interfaces, EF Core gobernaría la arquitectura y el dominio quedaría contaminado.


4. Repositorios: Puertos para acceder a datos del dominio

Un Repositorio es un tipo especial de contrato cuyo propósito es proporcionar acceso a entidades del dominio como si fueran colecciones en memoria.

Los repositorios pertenecen al Dominio, NO a la infraestructura.

Ejemplos en ControlEscolar:

  • Acceder a materias del catálogo
  • Leer o guardar inscripciones
  • Obtener actividades de una inscripción

Ejemplo:

public interface IInscripcionRepository {
    Task<Inscripcion?> ObtenerInscripcionActivaAsync(MateriaId materiaId, PeriodoLectivo periodo);
    Task<List<Inscripcion>> ObtenerHistorialAsync(MateriaId materiaId);
    Task GuardarAsync(Inscripcion inscripcion);
}

La implementación técnica vive en Infrastructure.


5. Servicios en la Arquitectura Hexagonal

Existen dos tipos:


5.1 Servicios de Dominio

Son clases que encapsulan reglas de negocio puras, que no pertenecen naturalmente a una sola entidad o value object.

  • No conocen repositorios.
  • No acceden a infraestructura.
  • Solo contienen lógica del dominio.

Ejemplo (simplificado):

public static class InscripcionRules {
    public static bool PuedeInscribirse(Materia materia, Historial historial) {
        return historial.Intentos < 3 && historial.PrerrequisitoAprobado;
    }
}

5.2 Servicios de Aplicación

Son los orquestadores de casos de uso.
Se encuentran fuera del dominio y SÍ utilizan repositorios.

Ejemplo:

public class InscribirMateriaHandler {
    private readonly IMateriaRepository _materias;
    private readonly IInscripcionRepository _inscripciones;

    public async Task Ejecutar(InscribirMateriaCommand cmd) {
        var materia = await _materias.ObtenerPorCodigoAsync(cmd.Codigo);
        var historial = await _inscripciones.ObtenerHistorialAsync(materia.Id);

        // reglas de negocio (dominio)
        if (!InscripcionRules.PuedeInscribirse(materia, historial))
            throw new Exception("No puedes inscribirte.");

        var insc = new Inscripcion(materia.Id, cmd.Periodo, cmd.Asesor);
        await _inscripciones.GuardarAsync(insc);
    }
}

6. Adaptadores (Adapters): Implementaciones técnicas

La infraestructura implementa los contratos definidos en el dominio usando la tecnología elegida.

Ejemplo con SQL Server + EF Core:

public class SqlMateriaRepository : IMateriaRepository {
    private readonly AppDbContext _db;

    public SqlMateriaRepository(AppDbContext db) {
        _db = db;
    }

    public Task<List<Materia>> ListarTodasAsync() =>
        _db.Materias.ToListAsync();
}

El dominio nunca ve esta clase; solo ve la interfaz.


7. Contratos definidos para ControlEscolar

Basado en el dominio v1.0, los contratos iniciales recomendados son:

IMateriaRepository

public interface IMateriaRepository {
    Task<Materia?> ObtenerPorIdAsync(Guid id);
    Task<Materia?> ObtenerPorCodigoAsync(CodigoMateria codigo);
    Task<List<Materia>> ListarTodasAsync();
}

IInscripcionRepository

public interface IInscripcionRepository {
    Task<Inscripcion?> ObtenerInscripcionActivaAsync(Guid materiaId, PeriodoLectivo periodo);
    Task<List<Inscripcion>> ObtenerHistorialAsync(Guid materiaId);
    Task GuardarAsync(Inscripcion inscripcion);
}

IActividadRepository (dependiendo si decides separar actividades)

public interface IActividadRepository {
    Task<List<Actividad>> ObtenerPorInscripcionAsync(Guid inscripcionId);
    Task GuardarAsync(Actividad actividad);
}

IPeriodoLectivoProvider (opcional)

Para obtener el periodo actual cuando no se pasa explícitamente:

public interface IPeriodoLectivoProvider {
    PeriodoLectivo ObtenerPeriodoActual();
}

8. Resumen visual de Hexagonal Architecture

          +------------------+
          |     Dominio      |
          |  Entidades, VO,  |
          |   Servicios,     |
          |   Interfaces     |
          +--------+---------+
                   ^
                   |
             (Ports/Interfaces)
                   |
         +---------+----------+
         |    Aplicación      |
         |  Casos de uso      |
         +---------+----------+
                   ^
                   |
           Implementaciones
            (Adapters)
                   |
       +-----------+--------------+
       |   Infraestructura         |
       | SQL Server, EF, Archivos |
       +---------------------------+

9. Conclusiones

En ControlEscolar, la Arquitectura Hexagonal permite:

  • Diseñar reglas académicas coherentes sin contaminar el dominio con tecnología.
  • Cambiar fácilmente de SQL Server a PostgreSQL o a un repositorio en memoria.
  • Probar la lógica sin base de datos.
  • Mantener el sistema limpio, modular y extensible.

Los contratos (interfaces), los Value Objects, los Repositorios y los Servicios son los pilares que permiten esta separación estricta.

Con el dominio ya definido y los contratos establecidos, el siguiente paso es comenzar a escribir la capa de Domain en .NET 10 LTS.

Read more