Skip to content

Taller 1

Juan José Álvarez edited this page Aug 14, 2026 · 1 revision

Implementación del Patrón Creacional

Módulo: Creación de Sesión de Estudio Adaptativa Servicio: services/agente-tutor Taller 01 — Refactorización Arquitectónica


Problema

La creación de una sesión de práctica era el flujo más enredado del proyecto. Todo ocurría dentro de la vista:

  • Consultar el progreso del usuario en base de datos.
  • Decidir la dificultad con una cadena de if/else sobre la tasa de acierto.
  • Verificar que el nivel HSK solicitado estuviera desbloqueado.
  • Elegir caracteres de refuerzo ordenando manualmente un diccionario de errores.
  • Elegir caracteres nuevos según el nivel.
  • Crear la sesión, crear cada ejercicio uno por uno.
  • Enviar la notificación al usuario.

Consecuencias concretas: la vista superaba las 80 líneas, no se podía testear sin base de datos ni sin gastar llamadas al modelo de IA, y cambiar de "mandar correo real" a "solo imprimir en consola" implicaba comentar líneas a mano antes de cada demo.


Solución arquitectónica

Diagrama de interacción

sequenceDiagram
    participant C as Cliente (PWA)
    participant V as CrearSesionEstudioView
    participant S as SesionEstudioService
    participant F as Factories
    participant M as MotorTutor
    participant B as SesionEstudioBuilder
    participant N as Notificador

    C->>V: POST /api/sesiones/
    V->>V: Serializer valida forma del request
    V->>S: crear_sesion_adaptativa(usuario, nivel)

    S->>F: MotorTutorFactory.crear()
    F-->>S: MotorTutorMock | MotorTutorLangGraph
    S->>F: NotificadorFactory.crear()
    F-->>S: NotificadorConsola | NotificadorEmail

    S->>M: sugerir_dificultad(progreso)
    M-->>S: dificultad

    S->>B: para_usuario().en_nivel().con_dificultad()
    S->>B: agregar_refuerzos().agregar_contenido_nuevo()
    S->>B: build()
    B->>B: validar invariantes
    B-->>S: SesionEstudio persistida

    S->>N: sesion_lista(usuario, sesion)
    S-->>V: sesion
    V-->>C: 201 Created
Loading

Diagrama de clases

classDiagram
    class CrearSesionEstudioView {
        -service: SesionEstudioService
        +post(request) Response
    }

    class SesionEstudioService {
        -_motor: MotorTutor
        -_notificador: Notificador
        -_progreso: ProgresoRepository
        +crear_sesion_adaptativa(usuario, nivel, duracion)
    }

    class SesionEstudioBuilder {
        -_ejercicios: list
        +para_usuario() self
        +en_nivel() self
        +con_dificultad() self
        +agregar_refuerzos() self
        +agregar_contenido_nuevo() self
        -_validar()
        +build() SesionEstudio
    }

    class MotorTutorFactory {
        +crear()$ MotorTutor
    }

    class NotificadorFactory {
        +crear()$ Notificador
    }

    class MotorTutor {
        <<abstract>>
        +sugerir_dificultad(progreso)*
        +seleccionar_caracteres_nuevos(progreso, n)*
    }

    class Notificador {
        <<abstract>>
        +sesion_lista(usuario, sesion)*
    }

    CrearSesionEstudioView --> SesionEstudioService
    SesionEstudioService --> SesionEstudioBuilder
    SesionEstudioService --> MotorTutorFactory
    SesionEstudioService --> NotificadorFactory
    MotorTutorFactory ..> MotorTutor
    NotificadorFactory ..> Notificador
    MotorTutor <|-- MotorTutorMock
    MotorTutor <|-- MotorTutorLangGraph
    Notificador <|-- NotificadorConsola
    Notificador <|-- NotificadorEmail
Loading

Capas implementadas

Capa Archivo Responsabilidad
Interfaz tutor/views.py Capturar el request y delegar. 12 líneas de cuerpo.
Aplicación tutor/services.py Algoritmo de negocio. Orquesta Builder y Factory.
Dominio tutor/domain/builders.py Construcción válida de la sesión (Builder).
Dominio tutor/domain/exceptions.py Errores de negocio, independientes de HTTP.
Infraestructura tutor/infra/factories.py Decide qué implementación concreta se usa.
Infraestructura tutor/infra/motores.py Motor de tutoría: mock y real.
Infraestructura tutor/infra/notificadores.py Notificación: consola y correo.
Datos tutor/repositories.py Aísla el acceso a ProgresoUsuario.

Patrón Factory

MotorTutorFactory y NotificadorFactory concentran toda la decisión sobre qué implementación instanciar. El cambio de comportamiento se controla desde variables de entorno, sin tocar una línea de código:

# .env — desarrollo
TUTOR_ENGINE=MOCK
NOTIFICADOR=CONSOLE

# .env — producción
TUTOR_ENGINE=REAL
NOTIFICADOR=EMAIL
EMAIL_REMITENTE=no-reply@hanzimentor.app
# infra/factories.py
class MotorTutorFactory:
    @staticmethod
    def crear() -> MotorTutor:
        modo = os.getenv("TUTOR_ENGINE", "MOCK").upper()
        if modo == "REAL":
            from ..agent.graph import construir_agente
            return MotorTutorLangGraph(agente=construir_agente())
        return MotorTutorMock()

Ganancia medible: correr la suite de tests dejó de costar llamadas a la API del modelo, y las demos en clase no dependen de que haya conexión a internet.


Patrón Builder

SesionEstudioBuilder usa Fluent Interface (cada método devuelve self) y garantiza la invariante central del dominio: nada se persiste hasta que la sesión sea válida.

# services.py — así se lee el algoritmo de negocio
sesion = (
    SesionEstudioBuilder()
    .para_usuario(usuario_id, progreso["nivel_max_desbloqueado"])
    .en_nivel(nivel_hsk)
    .con_dificultad(dificultad)
    .con_duracion(duracion_min)
    .agregar_refuerzos(refuerzos)
    .agregar_contenido_nuevo(nuevos)
    .build()
)

build() ejecuta _validar() antes de tocar la base de datos y está envuelto en @transaction.atomic, de modo que una sesión sin sus ejercicios nunca puede quedar guardada a medias. Las reglas verificadas:

  • La sesión tiene usuario y dificultad definidos.
  • El nivel HSK está entre 1 y 6.
  • El nivel solicitado está desbloqueado para ese usuario.
  • Hay entre 3 y 20 ejercicios.

SOLID aplicado

Responsabilidad Única. La vista traduce HTTP. El servicio decide el flujo. El builder construye. Las factories eligen. Cada archivo tiene una razón para cambiar.

Abierto/Cerrado. Agregar un NotificadorPush no requiere modificar el servicio: se crea la clase y se añade una rama en la factory.

Sustitución de Liskov. MotorTutorMock y MotorTutorLangGraph cumplen el mismo contrato MotorTutor; el servicio funciona igual con cualquiera.

Segregación de Interfaces. MotorTutor y Notificador son contratos mínimos, de dos y un método respectivamente. Nadie implementa métodos que no usa.

Inversión de Dependencias. El servicio depende de las abstracciones MotorTutor y Notificador, nunca de MotorTutorLangGraph o NotificadorEmail directamente. El constructor acepta las dependencias inyectadas:

def __init__(self, motor=None, notificador=None, progreso_repo=None):
    self._motor = motor or MotorTutorFactory.crear()
    self._notificador = notificador or NotificadorFactory.crear()
    self._progreso = progreso_repo or ProgresoRepository()

Evidencia en tests

La inyección de dependencias permite probar el algoritmo completo con dobles, sin IA ni correos:

service = SesionEstudioService(
    motor=MotorFalso(),
    notificador=NotificadorEspia(),
    progreso_repo=ProgresoFalso(),
)

Casos cubiertos en tutor/tests.py:

  • La sesión creada incluye ejercicios de refuerzo antes que contenido nuevo.
  • El notificador se invoca exactamente una vez por sesión.
  • Solicitar un nivel bloqueado lanza NivelNoPermitidoError y no persiste nada.
  • El builder rechaza sesiones con menos de tres ejercicios.
python manage.py test tutor

Decisiones de diseño y sus trade-offs

Por qué Builder y no un constructor con parámetros. Una sesión se arma en varios pasos condicionales: puede tener refuerzos o no, contenido nuevo o no, dificultad calculada o forzada. Un constructor con siete argumentos opcionales sería ilegible y no podría validar el conjunto completo antes de guardar.

Por qué Factory y no inyectar desde settings.py. La factory concentra la decisión en un punto y permite construcción condicional real (el import de LangGraph solo ocurre si el modo es REAL, lo que evita cargar dependencias pesadas en desarrollo).

Costo asumido. La refactorización pasó de un archivo a ocho, y para un flujo trivial esto sería sobreingeniería. Se justifica aquí porque este es el flujo que más va a cambiar durante el semestre: el motor de tutoría todavía está en diseño y las reglas de dificultad se van a ajustar con datos de uso real.

Repository como capa extra. No lo pedía el taller, pero sin él el servicio quedaba acoplado al ORM de Django y los tests exigían base de datos. Con ProgresoRepository el algoritmo se prueba en memoria.


Archivos entregados

services/agente-tutor/tutor/
├── views.py                  # CBV delgada
├── services.py               # Service Layer
├── repositories.py           # Acceso a datos
├── serializers.py            # Validación de forma
├── urls.py
├── models.py
├── tests.py
├── domain/
│   ├── builders.py           # Patrón Builder
│   └── exceptions.py
└── infra/
    ├── factories.py          # Patrón Factory
    ├── motores.py            # MOCK | REAL
    └── notificadores.py      # CONSOLE | EMAIL