190 lines
5.3 KiB
Markdown
190 lines
5.3 KiB
Markdown
|
|
# 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)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
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
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
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
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
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
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
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
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
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)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
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
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
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 |
|