Módulo 8: Refactorizar con criterio (capstone)
5. Refactorizar en pasos pequeños y seguros
Descripción
Al terminar esta lección vas a tener la disciplina que hace posible todo lo que diagnosticaste en las lecciones 3 y 4: cambiar la estructura de un código sin cambiar su comportamiento, en pasos tan chicos que en ningún momento el sistema queda roto. Vas a saber qué es exactamente un "paso seguro" —tiene cuatro partes y se pueden nombrar—, vas a ver el refactor completo del bloque de proveedores de pago del checkout de Boletia en seis pasos, cada uno con lo que se verifica y con qué pasa si te detienes ahí, y vas a entender por qué el refactor gigante de una sentada —ese que se empieza un viernes con la mejor intención— casi siempre termina en un revert el lunes.
Esto importa porque es la lección que separa una propuesta de un resultado. En el trabajo real, la mayoría de las refactorizaciones correctas no fracasan por un mal diagnóstico: fracasan por la ejecución. Alguien diagnosticó bien, empezó bien, se le complicó a mitad de camino, tuvo que entregar otra cosa el jueves, dejó el trabajo a medias, y el sistema quedó con la mitad en el mundo nuevo y la mitad en el viejo —que es el peor de los tres estados posibles, peor que no haber empezado—. La técnica de esta lección existe para que ese escenario, que es inevitable porque el trabajo real siempre se interrumpe, no tenga consecuencias.
Y hay una idea de fondo que conviene tener clara desde ahora, porque cambia cómo se siente el trabajo. Refactorizar no es "mejorar el código": es una operación con una definición estricta —cambiar la estructura sin cambiar el comportamiento observable—. Todo lo que cambie comportamiento no es refactorizar, aunque sea una mejora: es un cambio de funcionalidad, y va aparte, en su propio trabajo, con su propia revisión. Esa frontera parece burocrática hasta la primera vez que un cambio mixto te falla en producción y no puedes saber cuál de las dos mitades lo causó.
Conexión con el módulo: la lección 2 te dio el permiso para tocar y la lección 3 y la 4 te dieron los dos diagnósticos. Esta te da las manos, y sirve igual para las dos direcciones: los seis pasos que vas a ver aquí sobre el checkout tienen la misma forma que los seis del desmontaje de plugins/ que viste en el módulo 2 —construir el camino nuevo, mover el tráfico, borrar lo que quedó sin uso—. La lección 6 te va a pedir la bitácora de estos pasos como parte de la justificación, porque es la prueba de que el sistema nunca estuvo roto. Y la lección 7 usa el costo real de este trabajo —tiempo, riesgo y revisión— para decidir cuándo no vale la pena hacerlo.
El restaurante que no cierra
Un restaurante con veinte años de barrio decide remodelar. Tiene dos maneras de hacerlo.
La primera: cerrar seis semanas. Se tira todo, se rehace todo, se reabre. Es más rápido en horas de obra y es la que se ve mejor en el plano. Y tiene tres problemas que no se ven en el plano: durante seis semanas no entra un peso; si a la tercera semana aparece un problema estructural y la obra se alarga, no hay marcha atrás porque la cocina ya no existe; y los clientes de veinte años, mientras tanto, encontraron otro lugar.
La segunda: remodelar por mitades, sin cerrar. Se cierra la mitad del salón con una mampara, se remodela, se abre esa mitad y se cierra la otra. Después la barra, en una madrugada. Después la cocina, montando primero una estación provisional que ya funciona antes de desconectar la vieja. Toma más tiempo total y en ningún momento el restaurante deja de servir.
Fíjate en la propiedad que tiene la segunda y que la primera no tiene: puedes parar en cualquier momento. Si a mitad del proyecto se acaba el dinero, o cambia el dueño, o el barrio deja de ser lo que era, el restaurante queda con medio salón nuevo y funcionando. En la primera opción, parar a mitad significa un local vacío con una cocina desconectada.
Esa propiedad —poder parar y quedar mejor que al principio— es el criterio entero de esta lección. No es una preferencia de estilo ni una recomendación de prudencia. Es la única forma de trabajo compatible con la realidad de un equipo, donde el trabajo se interrumpe siempre: por una urgencia, por una revisión que tarda, por un lanzamiento que se adelantó, por unas vacaciones.
Y hay un detalle más de la analogía que vale oro: la estación provisional de la cocina. Antes de desconectar la vieja, se monta la nueva y se comprueba que funciona. Durante unos días existen las dos. Eso, en código, tiene nombre —cambio en paralelo, o expand and contract— y es la técnica que hace que mover el tráfico de un camino a otro nunca tenga un momento de vacío.
Anatomía de un paso seguro
"Pasos pequeños" es un consejo inútil si no se dice qué es un paso. Un paso seguro tiene cuatro partes, y si le falta una, no es un paso: es un tramo de trabajo con la esperanza de que salga bien.
1. La precondición. Lo que tiene que ser cierto antes de empezarlo. En el paso "borra el registro de plugins", la precondición es "nadie lo llama". Si la precondición no se cumple, el paso no se hace: se hace primero el que la cumple. La mayoría de los desastres de refactorización son pasos ejecutados sin su precondición.
2. La transformación. Lo que haces, y tiene que ser lo más mecánico posible. Extraer una función, renombrar, mover un archivo, introducir un parámetro: transformaciones que tu editor puede hacer solo y que no dependen de que entiendas la lógica. Las transformaciones mecánicas son seguras porque no hay espacio para el criterio, y donde no hay criterio no hay error de criterio. Cuando un paso te obliga a pensar en la lógica del negocio, párate: ese paso es demasiado grande.
3. La verificación. Cómo compruebas que el comportamiento no cambió. Normalmente son las pruebas, corridas al terminar. Pero la verificación puede ser otra cosa —arrancar la aplicación, correr un script que compara la salida vieja con la nueva, mirar un registro en el entorno de pruebas— y lo importante es que sea específica y hecha ahora, no "lo probamos al final".
4. El punto de parada. El estado en el que queda el sistema si te vas a casa justo después. Y la regla que gobierna todo: ese estado tiene que ser un estado válido, con el sistema funcionando y las pruebas en verde. Si un paso no cumple esto, la solución es siempre la misma: pártelo en dos.
Aplícalo a un paso concreto y se ve claro:
Paso: mover la lógica del cobro con Stripe a la clase
StripeProvider. Precondición: la clase existe, cumple el contrato y tiene pruebas. Transformación: encheckout, reemplazar el cuerpo de la rama"stripe"por una llamada al proveedor. Verificación: las pruebas de caracterización del checkout siguen en verde. Punto de parada: el sistema funciona. Las otras dos ramas siguen con su código viejo, y eso está bien: conviven.
Esa última línea es la que más cuesta aceptar y la más importante. Durante un refactor bien hecho, el sistema está temporalmente inconsistente en su estilo —una rama nueva, dos viejas— y perfectamente consistente en su comportamiento. Quien no soporta esa inconsistencia estética termina haciendo el cambio entero de una vez, que es exactamente lo que queremos evitar.
La red: sin ella no estás refactorizando
Refactorizar significa cambiar la estructura sin cambiar el comportamiento. Esa promesa necesita alguien que la verifique, y no puedes ser tú leyendo el diff: los ojos no ven un >= que se volvió >.
La red son pruebas, y para este trabajo hay un tipo específico: las pruebas de caracterización. No dicen lo que el código debería hacer, sino lo que hace, incluidos sus defectos. Su único trabajo es avisarte si cambiaste algo sin querer.
# Archivo: tests/test_checkout_characterization.py
# Fijan el comportamiento ACTUAL del cobro, tal como es hoy.
# No juzgan si está bien: solo lo congelan mientras movemos la estructura.
def test_stripe_charges_in_integer_cents():
order = make_order(total=540.00, provider="stripe")
with fake_stripe() as api:
checkout(order)
assert api.last_charge["amount"] == 54000 # centavos, entero
assert api.last_charge["currency"] == "MXN"
assert order.status == "paid"
def test_mercadopago_charges_as_float_with_description():
order = make_order(total=540.00, provider="mercadopago", id=8812)
with fake_mercadopago() as api:
checkout(order)
assert api.last_payment["amount"] == 540.00 # float, no centavos
assert api.last_payment["description"] == "Boletia #8812"
def test_cash_does_not_charge_and_leaves_the_order_pending():
order = make_order(total=540.00, provider="cash")
checkout(order)
assert order.status == "pending"
assert order.reference is not None
def test_unknown_provider_raises():
with pytest.raises(ValueError):
checkout(make_order(provider="paypal"))
Tres cosas sobre estas pruebas, porque no son cualquier prueba.
Prueban el comportamiento observable, no la estructura. Ninguna menciona StripeProvider, ni el diccionario de proveedores, ni la clase que vas a crear. Por eso siguen sirviendo después del refactor: son la red, no el andamio. Una prueba que menciona la estructura no es una red: es una segunda cosa que hay que refactorizar.
Cubren el caso raro. test_cash_does_not_charge_and_leaves_the_order_pending es la más valiosa de las cuatro, porque el efectivo es el proveedor que menos se parece a los otros y es el que se rompe primero si el contrato queda mal.
Y cubren la cerca de la lección 2. Falta una quinta prueba, la del comportamiento antifraude de MercadoPago para montos altos, que investigaste en la lección 2 y que hoy no tiene ninguna. Escribirla es parte del paso 0: lo que descubriste leyendo se convierte en red antes de mover nada.
Cómo se construye una red cuando no hay ninguna, cómo se escriben pruebas rápidas y deterministas, cómo se sustituye una API externa en una prueba sin volverla frágil, y qué cubrir cuando no puedes cubrirlo todo, es el tema de testing-backend-applications-guide. Aquí la damos por disponible y nos concentramos en cómo se usa durante un refactor. Pero la regla de esta lección es dura y no admite matices: si no puedes escribir la red, ese es el trabajo, y va antes que el patrón. Refactorizar sin red no es refactorizar: es reescribir con los dedos cruzados.
Ejemplo trabajado: el refactor del cobro, en seis pasos
Vamos a mover el if por proveedor de pago fuera del corazón del checkout. El diagnóstico ya lo hiciste en la lección 3: eje confirmado, tres implementaciones reales, condicional repetido en cuatro archivos, peldaño 4. Ahora la ejecución.
Cada paso lleva sus cuatro partes y una línea que dice qué pasa si te detienes ahí.
Paso 0 — Pon la red y levanta el inventario.
Precondición: ninguna. Este paso siempre es el primero. Transformación: escribir las pruebas de caracterización de arriba, más la del comportamiento de MercadoPago. Y correr las cuatro búsquedas del inventario. Verificación: las pruebas pasan contra el código actual. Si alguna falla, entendiste mal el comportamiento y hay que corregir la prueba, no el código. Si paras aquí: el sistema quedó estrictamente mejor: tiene pruebas que antes no tenía. Este solo paso ya es un cambio entregable.
# Los cuatro lugares donde vive el conocimiento "qué proveedores existen".
grep -rn '"stripe"\|"mercadopago"\|"cash"' --include='*.py' .
grep -rn 'StripeClient\|MercadoPagoClient' --include='*.py' .
grep -rn 'STRIPE_KEY\|MP_TOKEN' --include='*.py' .
grep -rn '\.provider' --include='*.py' .
Resultado: checkout/checkout.py, admin/refunds.py, reports/reconciliation.py y api/routes.py. Anótalo antes de cambiar nada. Un refactor que empieza a la mitad del inventario termina con la mitad del sistema en el mundo nuevo y la otra mitad en el viejo.
Paso 1 — Escribe el contrato. No toca nada existente.
Precondición: conocer los tres casos, incluido el raro. Transformación: un archivo nuevo, con el contrato y la forma del resultado. Verificación: el proyecto sigue arrancando. Nada más, porque nada más cambió. Si paras aquí: hay un archivo nuevo que nadie usa. Cero riesgo.
# Archivo: payments/provider.py
from typing import Protocol
from dataclasses import dataclass
@dataclass(frozen=True)
class PaymentResult:
"""Lo que devuelve cualquier intento de cobro, sin importar el proveedor.
Inmutable a propósito: un resultado de pago es un hecho ocurrido.
"""
status: str # "succeeded" | "pending" | "failed"
external_id: str | None = None # el id del cobro en el sistema del proveedor
reference: str | None = None # solo para efectivo: la referencia de tienda
error_message: str | None = None
class PaymentProvider(Protocol):
"""El contrato común. Todo proveedor de pago de Boletia sabe hacer esto."""
def charge(self, order) -> PaymentResult:
...
def refund(self, order, amount: float) -> PaymentResult:
...
def daily_charges(self, day) -> list[dict]:
...
Que PaymentResult exista, y que tenga un status en vez de un booleano, sale del caso del efectivo: no cobra, deja la orden pendiente. El contrato se diseñó mirando el caso más raro, como viste en la lección 3.
Paso 2 — Mueve cada rama a su clase. Sigue sin tocar nada existente.
Precondición: el contrato escrito. Transformación: copiar el cuerpo de cada rama a una clase, sin cambiar la lógica. Verificación: pruebas unitarias nuevas de cada proveedor, con la API externa sustituida. Si paras aquí: hay código nuevo que nadie llama todavía. El sistema sigue funcionando exactamente igual.
# Archivo: payments/stripe_provider.py
class StripeProvider:
"""Cobra con Stripe. Todo lo raro de Stripe queda encerrado aquí."""
def __init__(self, api_key: str):
self._client = StripeClient(api_key=api_key)
def charge(self, order) -> PaymentResult:
# Stripe trabaja en centavos y como entero. La conversión vive aquí,
# que es el único lugar del sistema que tiene por qué saberlo.
charge = self._client.create_charge(
amount=int(round(order.total * 100)),
currency="MXN",
)
return PaymentResult(status="succeeded", external_id=charge["id"])
# Archivo: payments/mercadopago_provider.py
# MercadoPago revisa por antifraude las operaciones por encima de este monto
# y su API de consulta tarda 1-3 s en reflejar un cobro que ya aprobó.
# La conciliación de las 3:00 usa esa API y marcaba como faltantes órdenes
# ya pagadas (47 en enero de 2025, #2214). Confirmado por el proveedor
# (MP-88431). Se quita cuando su API de consulta sea consistente.
ANTIFRAUD_REVIEW_THRESHOLD = 10_000.0
REVIEW_SETTLE_SECONDS = 2
class MercadoPagoProvider:
def __init__(self, token: str):
self._client = MercadoPagoClient(token=token)
def charge(self, order) -> PaymentResult:
payment = self._client.pay(order.total, description=f"Boletia #{order.id}")
if order.total > ANTIFRAUD_REVIEW_THRESHOLD:
time.sleep(REVIEW_SETTLE_SECONDS)
check = self._client.get_payment(payment.payment_id)
if check.status != "approved":
return PaymentResult(status="pending", external_id=payment.payment_id)
return PaymentResult(status="succeeded", external_id=payment.payment_id)
Mira dónde terminó la cerca de la lección 2: encapsulada en la clase del proveedor al que pertenece, con su comentario completo y su umbral con nombre. Antes vivía suelta en el corazón del checkout, protegida por un comentario de tres palabras. El refactor no la borró ni la conservó tal cual: la mudó a su casa. Ese es uno de los beneficios que más se subestiman de esta dirección de trabajo.
# Archivo: payments/cash_provider.py
class CashProvider:
"""Pago en efectivo en tienda. No cobra: genera una referencia y espera.
Es el proveedor que más se aleja de los otros dos, y por eso es el que
prueba si el contrato está bien diseñado.
"""
def __init__(self, store_chain: str):
self._store_chain = store_chain
def charge(self, order) -> PaymentResult:
# Aquí no entra dinero todavía: el cliente lleva la referencia a la
# tienda y la cadena nos avisa después. Por eso "pending".
reference = generate_cash_reference(order.id, chain=self._store_chain)
return PaymentResult(status="pending", reference=reference)
Paso 3 — Escribe el punto de elección.
Precondición: las tres clases existen y están probadas. Transformación: un diccionario de constructores y una función que resuelve por nombre. Verificación: una prueba que pide los tres nombres y comprueba que devuelve el tipo correcto, más una que comprueba el error con un nombre desconocido. Si paras aquí: sigue sin haber tráfico en el camino nuevo. Cero riesgo.
# Archivo: payments/factory.py
class UnknownProviderError(ValueError):
"""El proveedor pedido no existe. Error propio para poder distinguirlo."""
# Guardamos CÓMO construir, no el objeto construido: así nada se instancia al
# importar el módulo y las credenciales se leen solo cuando de verdad se usan.
_PROVIDERS: dict[str, callable] = {
"stripe": lambda: StripeProvider(api_key=settings.STRIPE_KEY),
"mercadopago": lambda: MercadoPagoProvider(token=settings.MP_TOKEN),
"cash": lambda: CashProvider(store_chain=settings.CASH_STORE_CHAIN),
}
def available_providers() -> list[str]:
"""Qué proveedores existen. Una sola fuente de la verdad, consultable."""
return sorted(_PROVIDERS)
def get_payment_provider(name: str) -> PaymentProvider:
"""El ÚNICO lugar del sistema que sabe qué proveedores existen."""
try:
build = _PROVIDERS[name]
except KeyError:
raise UnknownProviderError(
f"Proveedor de pago desconocido: {name!r}. "
f"Disponibles: {', '.join(available_providers())}"
) from None
return build()
Aquí termina la fase de expandir: el camino nuevo está construido, probado, y el viejo sigue intacto. Nadie ha corrido ningún riesgo todavía. Esta es la mitad del trabajo y la que se puede hacer con total tranquilidad un viernes por la tarde.
Paso 4 — Mueve el tráfico, un archivo por vez.
Precondición: el camino nuevo probado; el inventario del paso 0 en la mano. Transformación: en un archivo, reemplazar el condicional por la llamada a la fábrica. Verificación: las pruebas de caracterización de ese archivo, en verde. Si paras aquí: un archivo usa el camino nuevo y tres usan el viejo. Los dos caminos hacen exactamente lo mismo, así que el sistema es correcto aunque su estilo sea temporalmente mixto.
# Archivo: checkout/checkout.py — DESPUÉS
def charge_order(order) -> PaymentResult:
"""Cobra una orden. Ya no sabe qué proveedores existen ni cómo se construyen."""
provider = get_payment_provider(order.provider)
return provider.charge(order)
# Archivo: admin/refunds.py — DESPUÉS
def refund_order(order, amount: float) -> PaymentResult:
provider = get_payment_provider(order.provider)
return provider.refund(order, amount)
# Archivo: reports/reconciliation.py — DESPUÉS
def reconcile_day(day):
"""Compara lo que creemos que cobramos contra lo que dice cada proveedor."""
ours = load_paid_orders(day)
# Un regalo que no habíamos pedido: ahora la lista de proveedores se puede
# RECORRER en vez de escribirla a mano por cuarta vez.
theirs = []
for name in available_providers():
theirs.extend(get_payment_provider(name).daily_charges(day))
return compare(ours, theirs)
Cuatro archivos, cuatro cambios separados, cuatro revisiones de un minuto. Y el orden importa: empieza por el de menos riesgo —la conciliación, que corre de madrugada y es fácil de revisar— y deja el checkout para cuando ya hiciste dos y tienes confianza en el método. Nunca al revés.
Paso 5 — Borra el camino viejo, solo cuando nadie lo pisa.
Precondición: la búsqueda del paso 0, repetida, ya no devuelve ningún condicional por proveedor fuera de
payments/. Transformación: borrar el código muerto y los imports que quedaron colgando. Verificación: las pruebas completas, más arrancar la aplicación. Si paras aquí: el refactor está terminado.
Aquí termina la fase de contraer. Y nota la precondición: no es "creo que ya no lo usa nadie", es la búsqueda repetida devolviendo vacío. La búsqueda es tu permiso para borrar.
Qué esperar de este refactor. Cinco observaciones.
Primera: los pasos 1, 2 y 3 tienen riesgo cero. Son más de la mitad del trabajo y no tocan una línea de código existente. Esa asimetría es deliberada y es la esencia del cambio en paralelo: construye el camino nuevo entero antes de mover un solo coche. El riesgo se concentra en el paso 4, que es el más chico y el que se hace de a un archivo.
Segunda: en ningún momento el sistema estuvo roto. Después de cada uno de los seis pasos, Boletia arranca, cobra y las pruebas están en verde. Si tuvieras que abandonar el trabajo después del paso 2 —porque entró una urgencia y no volviste en un mes—, lo que queda son tres clases probadas que nadie usa. Es deuda ordenada y visible, no un sistema a medio migrar.
Tercera: la fase incómoda es real y hay que nombrarla. Entre el paso 4a y el 4d hay un rato —horas o días— en que dos archivos usan la fábrica y dos usan el if. Alguien va a mirar el código en ese momento y va a pensar "esto está inconsistente". Lo está, en estilo. No lo está en comportamiento, que es lo único que le importa a un usuario. Aguantar esa incomodidad es lo que permite trabajar en pasos chicos; no aguantarla es lo que produce el cambio gigante.
Cuarta: apareció un beneficio que no se había pedido. En reconciliation.py, la lista de proveedores dejó de escribirse a mano y pasó a recorrerse. Ese tipo de regalo aparece seguido cuando el conocimiento se concentra en un lugar, y conviene mencionarlo en la justificación —pero no conviene aprovechar el momento para agregar funcionalidad nueva—.
Quinta: la cerca sobrevivió y quedó mejor. El comportamiento antifraude de MercadoPago no se perdió en el camino, que era el riesgo real: ahora vive en la clase de su proveedor, con nombre, comentario y prueba. Compara eso con lo que habría pasado en una reescritura de una sentada, donde una línea rara sin prueba tiene todas las papeletas para desaparecer.
Por qué el refactor gigante termina mal
Vale la pena entender por qué falla el enfoque de "me tomo el viernes y lo hago todo", porque a todo el mundo le parece más eficiente y en la teoría lo es. Cinco razones, y ninguna es sobre la calidad de quien lo intenta.
Uno: la revisión se vuelve imposible. Un diff de mil doscientas líneas que mueve, renombra y transforma al mismo tiempo no se puede revisar de verdad. Quien revisa hace una de dos cosas: aprueba sin leer, o pide cambios genéricos porque no encuentra dónde agarrarse. Las dos son malas. En cambio, seis diffs de treinta a ochenta líneas se revisan bien, y cada uno recibe atención real. La revisión efectiva tiene un tamaño máximo, y está bastante por debajo de lo que uno cree.
Dos: cuando falla, no sabes qué falló. Si un cambio grande produce un error en producción, tienes mil doscientas líneas de sospechosos. Si el problema aparece después de un cambio de cuarenta líneas, tienes cuarenta. Esa diferencia no es de comodidad: decide si el diagnóstico toma diez minutos o toda la tarde con el sistema caído.
Tres: el trabajo se interrumpe, siempre. Una urgencia, una reunión, un lanzamiento, una gripe. Un refactor que solo tiene valor cuando está terminado es una apuesta contra la realidad de tu semana. Y cuando se retoma quince días después, la mitad del contexto se perdió y hay que reconstruirla.
Cuatro: los conflictos al integrar cambios crecen con el tiempo y con el tamaño. Un trabajo de dos semanas sobre el corazón del sistema choca con todo lo que el resto del equipo hizo mientras tanto. Y resolver un conflicto en medio de un refactor es especialmente peligroso, porque hay que decidir qué versión gana en un código que ya no se parece a ninguna de las dos.
Cinco, y es la más humana: el sesgo del compromiso. Cuando llevas tres días, admitir que el enfoque era equivocado cuesta muchísimo más que cuando llevas veinte minutos. Los pasos chicos no solo bajan el riesgo técnico: bajan el costo de cambiar de opinión, y cambiar de opinión a tiempo es una de las habilidades más rentables del oficio.
Hay una excepción y conviene decirla para que la regla no se vuelva dogma: cuando el rincón es pequeño, aislado y sin usuarios externos —una función de treinta líneas que solo llama un lugar—, hacerlo de una sentada es perfectamente razonable. El procedimiento completo es para código que está en el camino de algo que importa. La proporción también aplica aquí: el método tiene que ser proporcional al riesgo.
La bitácora: la prueba de que nunca estuvo roto
Un último elemento, chico y muy rentable. Mientras trabajas, ve anotando los pasos con lo que verificaste después de cada uno. No es documentación: son cinco líneas que se escriben solas.
1. Pruebas de caracterización del cobro (4 casos + antifraude MP). Verde.
2. payments/provider.py: contrato PaymentResult + PaymentProvider. No toca nada.
3. Tres proveedores en payments/*.py, con sus pruebas. Nadie los llama todavía.
4. payments/factory.py + pruebas de resolución. Camino nuevo listo.
5. reconciliation.py usa la fábrica. Verde. (empiezo por el de menos riesgo)
6. refunds.py usa la fábrica. Verde.
7. routes.py valida con available_providers(). Verde.
8. checkout.py usa la fábrica. Verde. (el último, el de más riesgo)
9. Borro los if muertos e imports colgantes. Búsqueda del paso 0 vacía. Verde.
Esa lista hace tres cosas, y las tres valen más de lo que cuesta escribirla. Demuestra el método: se ve que en ningún punto el sistema quedó roto y que el checkout se tocó al final. Le da a quien revisa un mapa para leer los cambios en orden. Y te protege a ti: si algo sale mal en producción tres días después, tienes la secuencia exacta y sabes por dónde empezar a mirar. La lección 6 te va a pedir esta bitácora como parte del entregable, y el proyecto final también.
Errores comunes
Mezclar refactorización con funcionalidad nueva (de método). Qué pasa: alguien está moviendo los proveedores a clases y, ya que está ahí, agrega el reintento automático que hacía falta, o corrige un redondeo que estaba mal. El cambio queda con dos naturalezas mezcladas y nadie puede revisarlo, porque no se distingue lo que se movió de lo que se transformó. Y si algo falla en producción, no hay forma de saber cuál de las dos mitades lo causó, así que la única salida es revertir todo —incluida la parte buena—. Por qué pasa: mientras estás dentro del archivo ves el defecto, arreglarlo cuesta dos minutos, y volver después se siente como desperdicio. Cómo detectarlo: si tu cambio hace que una prueba de caracterización falle, agregaste comportamiento, no refactorizaste. Esa es la prueba mecánica y no falla. Cómo corregirlo: anota el hallazgo en una lista y sigue. Los arreglos van después, sobre el código ya ordenado, donde además son más fáciles y más fáciles de probar. La regla de Kent Beck lo dice mejor que nadie: "haz que el cambio sea fácil, y después haz el cambio fácil" —dos movimientos, dos commits, en ese orden—.
Refactorizar sin red y llamarlo refactorizar (de riesgo). Qué pasa: no hay pruebas, escribirlas parece caro, y alguien decide avanzar "con cuidado", revisando el diff con atención. El resultado se ve bien y funciona en el 95% de los casos; el 5% restante son las ramas que nadie ejecutó —el efectivo, el evento sin asientos, el monto alto de MercadoPago— y aparecen en producción de a una durante las semanas siguientes, sin que nadie las relacione con el cambio. Por qué pasa: escribir la red se siente como trabajo previo al trabajo, y la revisión visual da una falsa sensación de control. Cómo detectarlo: si no puedes nombrar la prueba concreta que fallaría si te equivocas en este paso, no tienes red. Cómo corregirlo: escribe la red, aunque sea parcial. Y si de verdad no se puede —código imposible de ejecutar en aislamiento—, la salida es hacer el rincón testeable primero, con las técnicas de testing-backend-applications-guide. Ese trabajo previo es el refactor; lo demás viene solo.
Hacer pasos que no se pueden verificar (de tamaño). Qué pasa: alguien parte el trabajo en pasos, pero los parte por temas —"paso 1: los proveedores; paso 2: el precio; paso 3: las notificaciones"— y cada uno toca doscientas líneas y deja el sistema sin arrancar hasta el final. Ha dividido el trabajo sin dividir el riesgo. Por qué pasa: se confunde "pasos" con "capítulos". Un capítulo es una unidad de narración; un paso es una unidad de verificación. Cómo detectarlo: la pregunta de control es una sola —si me voy a casa ahora mismo, ¿el sistema funciona?—. Si la respuesta es no, no era un paso. Cómo corregirlo: parte por el punto de verificación, no por el tema. Y si un paso no se puede hacer sin romper algo, la solución es siempre la misma: pártelo en dos, con una fase de expandir que agrega el camino nuevo y una de contraer que quita el viejo.
Ejercicios
Ejercicio 1 — ¿Es un paso seguro? Para cada uno, di si cumple las cuatro partes y, si no, cómo lo partirías.
(a) "Renombrar calculate_line_price a price_for_ticket en los seis lugares donde aparece."
(b) "Borrar plugins/base.py y quitar la herencia de DefaultSeatingPlugin."
(c) "Crear payments/factory.py con el diccionario de constructores."
(d) "Cambiar el checkout para que use la fábrica y, de paso, quitar los if de los otros tres archivos."
(e) "Convertir Ticket.kind de texto libre a un enum, migrando los valores existentes."
Ver solución
(a) Paso seguro. Precondición: ninguna. Transformación: mecánica y automatizable —tu editor la hace—. Verificación: las pruebas. Punto de parada: válido. Es el tipo de paso ideal: no hay espacio para el criterio, así que no hay error de criterio posible.
(b) No es seguro tal como está enunciado, y es el error del módulo 2 en su forma pura: empieza por la base de la pirámide. Su precondición —"nadie depende de esa herencia"— no se ha comprobado. Y en plugins/ hay cinco usuarios, uno de ellos escondido detrás de una variable privada. Cómo partirlo: primero cortar el uso en los cuatro usuarios de producción, uno por cambio; después verificar que la búsqueda por módulo está vacía; y solo entonces borrar la clase base. De afuera hacia adentro.
(c) Paso seguro, y del mejor tipo: riesgo cero. No toca nada existente. Su verificación es sencilla —una prueba que pide los tres nombres— y si paras ahí, hay un archivo nuevo que nadie usa.
(d) No es un paso: son cuatro. Y además está mal ordenado, porque pone el checkout primero, que es el de más riesgo. La forma correcta: un archivo por cambio, empezando por el de menos riesgo. El "de paso" del enunciado es la señal de alarma: en refactorización, "de paso" siempre significa que estás juntando dos pasos.
(e) No es un refactor. Cambia el modelo de datos y exige migrar valores existentes, así que ni siquiera cumple la definición —cambiar estructura sin cambiar comportamiento—: los datos cambian. Necesita el procedimiento de expandir y contraer en tres tiempos, con la regla de que el código es reversible y los datos no. Y sobre todo, no va mezclado con el refactor de precios, aunque toque el mismo campo.
Por qué funciona: dos de los cinco son pasos seguros, dos hay que partirlos y uno no es un refactor. Esa proporción es la que te vas a encontrar cuando planifiques trabajo real, y darte cuenta antes de empezar es lo que evita la mayoría de los problemas.
Ejercicio 2 — Revisa una bitácora ajena. Un compañero entrega este trabajo. Encuentra los tres problemas y di qué se rompió o pudo romperse en cada uno.
commit 1 Crea payments/ con el contrato y los tres proveedores
commit 2 Reemplaza los if de proveedor en checkout, refunds, reconciliation y routes
commit 3 Borra el código viejo y arregla imports
commit 4 Agrega reintento automático cuando Stripe devuelve 503
commit 5 Agrega tests de payments/
Ver solución
Problema 1 — las pruebas están al final (commit 5). No hubo red en ningún momento. Y peor: esas pruebas verifican el código nuevo, es decir, prueban lo que él escribió, no lo que había. Si el comportamiento cambió en el camino —por ejemplo, si el efectivo dejó de quedar en "pending"—, las pruebas nuevas lo consagrarían como correcto. El paso 0 existe exactamente para esto: la red se escribe antes y contra el código actual.
Problema 2 — el commit 2 mueve cuatro archivos de una vez. Es el paso de más riesgo del refactor, hecho en un solo movimiento y sin red. Si algo falla en producción, los sospechosos son cuatro archivos en vez de uno, y uno de ellos es el checkout. Además, no hay forma de revertir solo la parte que falló. Lo correcto: cuatro cambios, empezando por la conciliación y terminando por el checkout.
Problema 3 — el commit 4 mezcla funcionalidad con refactorización. El reintento ante un 503 es una mejora real y probablemente buena, y no es este trabajo. Metido aquí, contamina el diff: quien revise ya no puede confiar en que el comportamiento es idéntico, que era la única promesa del refactor. Y si el reintento causa un problema —por ejemplo, un cobro duplicado—, la reacción natural será revertir todo el trabajo, incluida la parte buena.
Y un detalle adicional que vale la pena notar: no hay ningún commit de inventario ni ninguna búsqueda documentada. No sabemos si los cuatro archivos eran todos. Si había un quinto —un script de administración, una tarea programada—, sigue con su if viejo, y ese if va a quedar desactualizado la próxima vez que se agregue un proveedor. Ese fallo aparece meses después y es dificilísimo de rastrear.
Por qué funciona: revisar el trabajo ajeno es la mejor forma de calibrar el propio. Los tres problemas de esta bitácora son los tres errores comunes de la lección en su forma más pura, y los tres son invisibles si solo miras el código final —que probablemente esté bien—. El método deja rastro en la bitácora, no en el resultado.
Ejercicio 3 — Parte un paso grande. Te toca este trabajo: "el checkout llama a los tres canales de notificación por su nombre y también al organizador y a analítica; hay que sacarlos de ahí". Escribe la secuencia de pasos con sus cuatro partes, y marca cuáles tienen riesgo cero. Pista: los avisos tienen una particularidad que el cobro no tiene —si uno falla, hoy se cae el checkout entero—.
Ver solución
Una secuencia que funciona:
Paso 0 — Red. Pruebas de caracterización que fijen a quién se le avisa hoy y bajo qué condición: al cliente por correo siempre; por SMS solo si tiene teléfono; por push solo si tiene token; al organizador por correo; y a analítica. Y una prueba del comportamiento actual ante un fallo: hoy, si el envío del SMS revienta, el checkout revienta. Eso es comportamiento observable y hay que congelarlo aunque no nos guste, porque cambiarlo es otro trabajo. Riesgo cero.
Paso 1 — Extraer. Mover el bloque de avisos, tal cual, a una función notify_order_paid(order, customer, event) en notifications/notifier.py. El checkout queda con una llamada. Transformación mecánica —extraer función—, verificación con las pruebas del paso 0. Este solo paso ya entrega la mitad del valor: el corazón del sistema adelgazó y los avisos tienen casa.
Paso 2 — Definir el contrato del suscriptor. Un archivo nuevo con la forma que tiene "alguien interesado en que una orden se pagó". No toca nada. Riesgo cero.
Paso 3 — Escribir cada interesado como suscriptor, con la lógica copiada tal cual, incluida su condición de disponibilidad. Nadie los llama todavía. Riesgo cero.
Paso 4 — Registrar los suscriptores y publicar el evento, en un solo lugar, reemplazando el cuerpo de notify_order_paid. Verificación: las pruebas del paso 0, sin cambiar una letra. Este es el único paso con riesgo real, y es chico.
Paso 5 — Borrar el código viejo. Precondición: la búsqueda no encuentra llamadas directas a los canales desde checkout.
Y el paso que NO va aquí: aislar los fallos para que un aviso caído no tumbe una venta. Es una mejora excelente, es probablemente la razón por la que alguien pidió este trabajo, y cambia comportamiento: hoy el checkout se cae, mañana no. Va después, sobre el código ya ordenado, en su propio cambio y con su propia prueba. Sobre la nueva estructura cuesta diez líneas; sobre la vieja habría sido un try/except por canal metido en el corazón del sistema.
Por qué funciona: la secuencia tiene cuatro pasos de riesgo cero y uno de riesgo real, igual que el ejemplo trabajado. Esa forma —mucho trabajo seguro, un momento chico de riesgo— no es casualidad: es lo que produce el cambio en paralelo, y es la razón por la que "refactorizar da miedo" deja de ser cierto cuando el método es el correcto.
Resumen y siguiente paso
En esta lección te llevas la disciplina que hace posible todo lo demás. Refactorizar tiene una definición estricta —cambiar la estructura sin cambiar el comportamiento observable— y todo lo que cambie comportamiento, aunque sea una mejora, es otra cosa y va aparte.
Un paso seguro tiene cuatro partes: una precondición que debe cumplirse antes; una transformación lo más mecánica posible, del tipo que hace tu editor; una verificación específica y hecha ahora; y un punto de parada que deja el sistema funcionando. Si un paso no cumple la cuarta, la respuesta es siempre la misma: pártelo en dos.
Viste la red —pruebas de caracterización, que fijan lo que el código hace y no lo que debería hacer— con sus tres propiedades: prueban comportamiento y no estructura, cubren el caso raro, y recogen lo que descubriste leyendo en la lección 2. Cómo construirla es de testing-backend-applications-guide; la regla de aquí es que sin red no estás refactorizando, estás reescribiendo con los dedos cruzados.
Ejecutaste el refactor completo del cobro de Boletia en seis pasos, con la forma del cambio en paralelo: construir el camino nuevo entero —pasos 1 a 3, riesgo cero—, mover el tráfico de a un archivo empezando por el de menos riesgo y dejando el checkout para el final, y contraer borrando lo viejo solo cuando la búsqueda vuelve vacía. En ningún momento el sistema estuvo roto, la cerca de MercadoPago sobrevivió y quedó mejor —con nombre, comentario y prueba, dentro de la clase de su proveedor— y apareció un beneficio que nadie había pedido en la conciliación.
Y entendiste por qué el refactor gigante falla, con cinco razones que no dependen de la habilidad: la revisión se vuelve imposible, el diagnóstico de un fallo se vuelve caro, el trabajo se interrumpe siempre, los conflictos crecen con el tamaño, y el costo de cambiar de opinión sube con cada día invertido. Más la excepción honesta: en un rincón chico y aislado, hacerlo de una vez es razonable, porque el método tiene que ser proporcional al riesgo.
Antes de avanzar deberías poder: nombrar las cuatro partes de un paso seguro; explicar qué es una prueba de caracterización y por qué no debe mencionar la estructura; describir la forma expandir/contraer con el orden de los archivos; y escribir la bitácora de un refactor que hayas hecho.
Ya sabes diagnosticar y ya sabes ejecutar. Falta lo que decide si tu trabajo se aprueba o se queda tres semanas esperando: cómo se defiende. La lección 6 se ocupa de la justificación, y su tesis es incómoda para quien acaba de aprender un vocabulario nuevo: el nombre del patrón no justifica nada. Nadie aprueba un cambio porque diga "apliqué Strategy". Se aprueba porque dice "agregar un proveedor tocaba cuatro archivos y ahora toca uno; el costo es un archivo más y un salto al leer; lo revertiría si dejáramos de tener más de dos proveedores". El nombre es la etiqueta; el argumento es el tradeoff.
Recursos
- Refactoring: Improving the Design of Existing Code (Martin Fowler) — la fuente de la definición estricta y del catálogo de movimientos mecánicos. Cada refactorización del libro viene con su procedimiento paso a paso y sus condiciones de seguridad; leer dos o tres completas enseña más sobre el tamaño de un paso que cualquier explicación general.
- Working Effectively with Legacy Code (Michael Feathers) — de donde vienen las pruebas de caracterización y las técnicas para hacer testeable un código que no lo es. Es el complemento exacto del paso 0.
- Parallel Change / Expand and Contract (Martin Fowler) — la formalización de expandir, migrar y contraer, que es la forma de los seis pasos de esta lección y la única manera segura de cambiar algo de lo que otros dependen.
testing-backend-applications-guide— la guía hermana donde se construye la red: cómo escribir pruebas rápidas y deterministas, cómo sustituir dependencias externas sin volverlas frágiles, y qué cubrir cuando no puedes cubrirlo todo. Esta lección la usa; aquella la enseña.- Make the change easy, then make the easy change (Kent Beck) — la frase que ordena el trabajo en dos movimientos y en ese orden. Es el antídoto del primer error común de esta lección.