Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — cierre del módulo de arquitectura Prerrequisito: Lección 27 — Configuración 12-factor
Objetivos
- Ejecutar un refactor real respaldado por la suite: cambiar el interior sin mover el contrato observable.
- Aplicar la técnica de " caracterización + feigning": fijar el comportamiento ACTUAL (incluidos los bugs) antes de tocar nada.
- Preparar el terreno del módulo 6: extraer el job de expiración de reservas hacia Celery sin regresar nada.
1. Qué es y qué no es refactor
Refactor (Fowler): cambiar la estructura interna sin cambiar el comportamiento observable. "Observable" aquí = la suite: los tests de contrato de la 15/26, las respuestas HTTP, los eventos del outbox de la 25, los logs con trace_id de la 45. Si un refactor cambia una respuesta, no era un refactor: era un cambio de comportamiento disfrazado (y necesita su propia tarea, su test nuevo y su versión de API, 14).
El anti-patrón del junior: "aprovecho el refactor para arreglar el bug". No: el bug se arregla en un commit con test de regresión ANTES, y el refactor va después sobre comportamiento ya fijado. Mezclar ambos = cuando algo se rompe no sabes si fue el refactor o el fix.
2. La red: qué cubre cada capa de tests
Antes de refactorizar, audita tu red (la 33 la teje a conciencia; aquí solo verificas que existe):
Unitarias (33) → lógica pura: FakeClock, resumen(), reglas de máquina de estados
Integración (34) → servicio + Postgres real: reservar() con transacciones y locks
Contrato (15/26) → respuestas problem+json, shape de la API: el refactor no las toca
E2E (35) → flujo completo de compra (lento, pocas, el seguro final)Si el código que vas a tocar no tiene red, la técnica es la caracterización (Feathers): escribe tests que FIJEN el comportamiento actual, aunque sea feo. assertEqual(resumen(r).total, 4250) — ni siquiera sabes si 4250 es correcto; solo sabes que era eso. Estos tests son pinos temporales: el refactor no debe moverlos; después, con el código limpio, los reescribes con las expectativas correctas.
3. El refactor guiado, paso a paso
Caso real del curso: la vista de reserva (la gorda de la 24) ya es delgada, pero reservar() acumuló 180 líneas tras la 25 (outbox, gateway, clock, locks). Objetivo: partirla en ReservacionService con dependencias claras sin cambiar NI UNA respuesta. La mecánica:
# 1. La suite entera en verde antes de tocar NADA (y cronometra: es tu línea base de duración)
python manage.py test --parallel 4
# 2. Branch del refactor (aunque el merge sea a trunk el mismo día)
git checkout -b refactor/reservacion-service
# 3. Commit pequeño = test en verde tras cada paso. Si rojo: revert del paso, no debugging
git commit -m "extraer _validar_asientos() sin cambios de comportamiento"Los pasos atómicos de este caso: (1) extraer _validar_asientos(event, seat_refs, repos) — lógica pura, unitaria nueva; (2) extraer _cobrar(gateway, amount, intent) — frontera de infra, la 24 ya definió el Protocol; (3) mover la transacción y el outbox a ReservacionService.reservar(); (4) la vista pasa de llamar a la función suelta a llamar al servicio — MODO de deprecación: la función vieja delega en el servicio durante un sprint y muere con su test de deprecación. Cada paso: suite verde → commit. Si un paso requiere "arreglar 12 tests", el paso era dos pasos.
4. Feigning: no cambies NADA observable
Los enemigos del refactor honesto: los "detalles" que sí son contrato. Checklist de no-rompibles: los status codes y bodies (26), los nombres de campos JSON (15), los IDs públicos (UUID, 13) — un refactor que renumera PKs visibles es un cambio de API. También lo son: los valores por defecto de config (27), los mensajes de error que el front parsea (si el front hace if err.type === "seat-unavailable", ese string es contrato), y los efectos de lado: emails, webhooks, outbox. La trampa clásica: reordenar el outbox y publicar ANTES del commit — los tests de la 25 (evento tras transacción) lo cazan; si no existieran, habrías roto la garantía sin saberlo.
Señal de refactor sano: git diff de los tests en el PR es ~cero (los de caracterización no se tocan); el diff del código es grande; el diff del output (capturas de respuestas en tests) es CERO.
5. El puente al módulo 6: la extracción del job
El cierre del curso propone: la expiración de reservas hoy es un cron manual (manage.py expirar_reservas corriendo en una pestaña de terminal, la vergüenza de la 00). El refactor que abre el módulo 6: extraer la LÓGICA (expirar_reservas(clock) en el servicio, puro, testeado con FakeClock) del DISPARADOR (el comando manage.py, después un worker Celery de la 29). La regla general: extrae primero la lógica del mecanismo — el refactor deja la lógica en el servicio con su firma estable, y el módulo 6 solo cambia quién la llama. Esto es también el patrón de toda migración de infraestructura: lógica estable, transporte sustituible.
Autoevaluación
- ¿Qué diferencia a un refactor de un cambio de comportamiento disfrazado y qué suite define "observable" en este curso?
- ¿Qué es un test de caracterización y por qué se permiten assertions "feos" (4250) que luego se reescriben?
- ¿Por qué "arreglar el bug de pasada" en el PR del refactor es la decisión que cuesta más caro?
- Enumera 5 cosas "observables" que un refactor NO puede cambiar aunque el diff de código sea legítimo.
- En la extracción del job de expiración: ¿qué se extrae primero (lógica o transporte) y qué deja preparado para la 29?
Continúa con los ejercicios. Las solutions.md solo tras intentarlo.