academia/docs/entregables/02-diseno/modelo-dominio/DI-002-modelo-dominio.md

5.3 KiB

DI-002: Modelo de Dominio

Proyecto: Sistema de Registro de Estudiantes Fecha: 2026-01-07


1. Diagrama de Entidades

┌─────────────────┐       ┌─────────────────┐
│    PROFESSOR    │       │     STUDENT     │
├─────────────────┤       ├─────────────────┤
│ Id: int (PK)    │       │ Id: int (PK)    │
│ Name: string    │       │ Name: string    │
└────────┬────────┘       │ Email: Email    │
         │                │ RowVersion      │
         │ 1:2            └────────┬────────┘
         ▼                         │
┌─────────────────┐                │ 0..3
│     SUBJECT     │                ▼
├─────────────────┤       ┌─────────────────┐
│ Id: int (PK)    │◄──────│   ENROLLMENT    │
│ Name: string    │  1:N  ├─────────────────┤
│ Credits: 3      │       │ Id: int (PK)    │
│ ProfessorId: FK │       │ StudentId: FK   │
└─────────────────┘       │ SubjectId: FK   │
                          │ EnrolledAt      │
                          └─────────────────┘

2. Entidades

Student (Aggregate Root)

public class Student
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public Email Email { get; private set; }
    public byte[] RowVersion { get; private set; }

    private readonly List<Enrollment> _enrollments = new();
    public IReadOnlyCollection<Enrollment> Enrollments => _enrollments;

    public int TotalCredits => _enrollments.Count * 3;

    public void Enroll(Subject subject, IEnrollmentPolicy policy)
    {
        policy.Validate(this, subject);
        _enrollments.Add(new Enrollment(this, subject));
    }

    public void Unenroll(int subjectId)
    {
        var enrollment = _enrollments.FirstOrDefault(e => e.SubjectId == subjectId);
        if (enrollment != null) _enrollments.Remove(enrollment);
    }
}

Subject

public class Subject
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public int Credits { get; } = 3;  // Constante: RN-002
    public int ProfessorId { get; private set; }
    public Professor Professor { get; private set; }
}

Professor

public class Professor
{
    public int Id { get; private set; }
    public string Name { get; private set; }

    private readonly List<Subject> _subjects = new();
    public IReadOnlyCollection<Subject> Subjects => _subjects;
}

Enrollment

public class Enrollment
{
    public int Id { get; private set; }
    public int StudentId { get; private set; }
    public int SubjectId { get; private set; }
    public DateTime EnrolledAt { get; private set; }

    public Student Student { get; private set; }
    public Subject Subject { get; private set; }
}

3. Value Objects

Email

public record Email
{
    public string Value { get; }

    private Email(string value) => Value = value;

    public static Result<Email> Create(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return Result.Failure<Email>("Email requerido");

        if (!Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"))
            return Result.Failure<Email>("Formato de email inválido");

        return Result.Success(new Email(email.ToLowerInvariant()));
    }
}

4. Domain Service

EnrollmentDomainService (Implementa RN-003 y RN-005)

public class EnrollmentDomainService : IEnrollmentPolicy
{
    public void Validate(Student student, Subject newSubject)
    {
        // RN-003: Máximo 3 materias
        if (student.Enrollments.Count >= 3)
            throw new MaxEnrollmentsExceededException(student.Id);

        // RN-005: No repetir profesor
        var professorIds = student.Enrollments
            .Select(e => e.Subject.ProfessorId)
            .ToHashSet();

        if (professorIds.Contains(newSubject.ProfessorId))
            throw new SameProfessorConstraintException(
                student.Id,
                newSubject.ProfessorId);
    }
}

5. Excepciones de Dominio

public class MaxEnrollmentsExceededException : DomainException
{
    public MaxEnrollmentsExceededException(int studentId)
        : base($"Estudiante {studentId} ya tiene 3 materias inscritas") { }
}

public class SameProfessorConstraintException : DomainException
{
    public SameProfessorConstraintException(int studentId, int professorId)
        : base($"Estudiante {studentId} ya tiene materia con profesor {professorId}") { }
}

6. Invariantes del Dominio

Invariante Entidad Validación
Email único Student DB Constraint + Validator
Email válido Student Value Object
Max 3 inscripciones Student Domain Service
No repetir profesor Student Domain Service
3 créditos/materia Subject Constante
2 materias/profesor Professor Seed Data