Módulo 5 · Arquitectura y código mantenible

Lección 28 — Refactorizar sin romper nada

Refactor apoyado en pruebas: cambiar el interior sin mover el contrato.

Publicada
En esta lección
  1. Objetivos
  2. 1. Qué es y qué no es refactor
  3. 2. La red: qué cubre cada capa de tests
  4. 3. El refactor guiado, paso a paso
  5. 4. Feigning: no cambies NADA observable
  6. 5. El puente al módulo 6: la extracción del job
  7. Autoevaluación

Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — cierre del módulo de arquitectura Prerrequisito: Lección 27 — Configuración 12-factor


Objetivos

  1. Ejecutar un refactor real respaldado por la suite: cambiar el interior sin mover el contrato observable.
  2. Aplicar la técnica de " caracterización + feigning": fijar el comportamiento ACTUAL (incluidos los bugs) antes de tocar nada.
  3. 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:

bash
# 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

  1. ¿Qué diferencia a un refactor de un cambio de comportamiento disfrazado y qué suite define "observable" en este curso?
  2. ¿Qué es un test de caracterización y por qué se permiten assertions "feos" (4250) que luego se reescriben?
  3. ¿Por qué "arreglar el bug de pasada" en el PR del refactor es la decisión que cuesta más caro?
  4. Enumera 5 cosas "observables" que un refactor NO puede cambiar aunque el diff de código sea legítimo.
  5. 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.