Módulo 8: Refactorizar con criterio (capstone)

8. Proyecto final: refactoriza un rincón de Boletia y defiéndelo

Descripción

Al terminar este proyecto vas a haber hecho, sobre el mismo sistema y en la misma semana, las dos cosas opuestas que definen esta guía: quitar una abstracción que no se gana su lugar y agregar la que de verdad emerge del problema. Y vas a haber escrito el documento que las defiende con su tradeoff, incluida la tercera decisión, la que casi nadie entrega: la lista de lo que encontraste y decidiste no tocar.

Este proyecto no se juzga por cuántos patrones apliques. Se juzga por dos cosas, y conviene decirlo de frente antes de que empieces. Primero, que el comportamiento observable no haya cambiado ni un carácter, demostrado con pruebas escritas antes de tocar nada. Segundo, que tu razonamiento sea defendible frente a alguien que no está de acuerdo. Puedes hacer un refactor impecable y entregarlo mal; puedes tener razón y no poder demostrarlo. Las dos cosas se practican aquí, y las dos son las que se evalúan en un equipo real.

La razón de que sean dos rincones y no uno es la que instaló la lección 1. Alguien que solo sabe agregar estructura tiene un reflejo, no un criterio: su respuesta a todo es un patrón, y con el tiempo produce un plugins/ por rincón. Alguien que solo sabe quitarla tiene el reflejo contrario y produce un checkout de trescientas líneas. Un reflejo se reconoce porque da la misma respuesta antes de mirar los datos. Si tu documento puede contestar "¿por qué a uno le agregas una interfaz y al otro se la quitas?" con evidencia y no con preferencias, tienes criterio, y eso es defendible en una entrevista mucho mejor que una lista de patrones memorizados.

Conexión con el módulo: este proyecto usa las siete lecciones y no de adorno. La 2 te da el permiso para tocar y va a hacer que encuentres, dentro del checkout, una línea que parece absurda y no lo es. La 3 te da la prueba del eje y el procedimiento de cuatro tiempos con el que emerge el patrón del lado del checkout. La 4 te da las tres formas de estructura que sobra y las siete verificaciones previas del lado de plugins/. La 5 te da la disciplina de los pasos y la bitácora. La 6 te da la fórmula de la justificación y la plantilla de cinco secciones. Y la 7 te da la tercera decisión —lo que se deja en paz— con sus tres preguntas y sus disparadores.

Un martes cualquiera

El encargo llega así, en un mensaje del equipo:

"Dos cosas antes del trimestre. Marketing quiere abonos de temporada, o sea un quinto tipo de boleto, y la última vez que agregamos uno tardamos dos días buscando todos los if; además, en el cambio anterior se nos escapó la conciliación y estuvo dos meses sin contar el efectivo. Y por otro lado hay que agregar asignación de asientos por grupos, pero nadie quiere meterse en plugins/: la última vez que alguien tocó esa carpeta se cayó el checkout cuarenta minutos. ¿Puedes dejar los dos rincones en un estado en el que se pueda trabajar, antes de que agreguemos nada?"

Detente en la forma del encargo, porque es la forma real y contiene tres lecciones.

Nadie pidió "aplica patrones" ni "quita la sobre-ingeniería". Pidieron poder hacer dos trabajos que hoy están bloqueados. La deuda de diseño casi nunca se paga porque alguien decida pagarla: se paga cuando bloquea algo que sí importa, y ese es el mejor momento para proponerlo, porque hay una razón de negocio y no una preferencia estética.

Los dos rincones apuntan en direcciones opuestas y el encargo no lo dice. Diagnosticar cuál va en cuál dirección es tu trabajo, no información que te dieron. Aunque ya conozcas la respuesta por las lecciones anteriores, el entregable exige que levantes tú la evidencia.

Y pidieron dejarlos trabajables antes de agregar nada. Eso es correcto y conviene defenderlo por escrito: mezclar un refactor con una funcionalidad nueva produce un cambio que nadie puede revisar, porque no se distingue lo que se movió de lo que se agregó. Y si algo sale mal, tampoco se sabe cuál de las dos cosas lo causó.

Así que el trabajo tiene dos tiempos, y tú vas a hacer el primero:

  1. Dejar los dos rincones trabajables, sin cambiar el comportamiento. ← este proyecto.
  2. Agregar el quinto tipo de boleto y el modo de grupos, ahora sobre código ordenado. ← no es este proyecto, pero tu documento tiene que dejarlo preparado.

Ejemplo trabajado: el mismo martes, en pequeño

Antes de meterte con los rincones grandes, vamos a hacer uno chiquito de principio a fin —las dos direcciones y el documento— para que veas el método completo en algo que cabe en dos páginas.

Dirección A (quitar) — utils/config_wrapper.py.

# Archivo: utils/config_wrapper.py    (el archivo completo)
import os


def get(key):
    return os.environ.get(key)

Doce puntos de llamada. La prueba invertida de la lección 4: si borro esta capa, ¿qué tendría que saber quien llama? Quien escribía config_wrapper.get("STRIPE_KEY") escribiría os.environ.get("STRIPE_KEY"). Nada. Redirige, no oculta. Y encima agrega un concepto propio del proyecto que hay que aprender para no escribir una llamada estándar.

Verificaciones previas: no hay segunda implementación en ningún entorno; no es frontera pública; ningún framework la exige; el historial dice tres años y ningún cambio, así que no hay obra en marcha. Sobra. Se quita de afuera hacia adentro: doce puntos de llamada en tres cambios agrupados por área, y al final se borra el archivo cuando la búsqueda vuelve vacía.

Dirección B (agregar) — reports/.

# Archivo: reports/exporter.py    (fragmento)

def export(event_id, fmt):
    if fmt == "csv":
        rows = load_attendees(event_id)
        rows = sort_by_name(rows)
        return write_file(to_csv(rows), ".csv")
    elif fmt == "pdf":
        rows = load_attendees(event_id)
        rows = sort_by_name(rows)
        return write_file(to_pdf(rows), ".pdf")
    elif fmt == "xlsx":
        rows = load_attendees(event_id)
        rows = sort_by_name(rows)
        return write_file(to_xlsx(rows), ".xlsx")
    else:
        raise ValueError(f"Formato desconocido: {fmt}")

Prueba del eje: las tres ramas contestan la misma pregunta (sí); cambian por separado —el generador de PDF cambió sin tocar el de CSV— (sí); la lista creció el año pasado con XLSX (sí, con evidencia). Y la señal 2: el mismo if de formatos está repetido en api/routes.py para validar y en admin/panel.py para el menú. Agregar un formato toca tres archivos.

Los cuatro tiempos: red de caracterización; extraer cada rama a una función; mirar las firmas —y descubrir que las tres comparten esqueleto y solo difieren en un paso—; y el punto de elección como un diccionario {"csv": to_csv, "pdf": to_pdf, "xlsx": to_xlsx} que routes.py y panel.py pueden preguntar en vez de repetir. Peldaño 3: en Python, tres funciones en un diccionario resuelven esto sin ninguna clase.

Y la tercera decisión — utils/dates.py. Feo, 140 líneas, parseo a mano de cuatro formatos. Las tres preguntas: 2 commits en 3 años; no bloquea nada; nadie lo lee. Déjalo y anótalo, con sus disparadores.

Y ahora el documento, que es más largo que los tres diffs juntos y así debe ser:

Contexto. Boletia es un servicio único, desplegado por nosotros, mantenido por seis personas. Nadie fuera de este repositorio consume este código.

Qué costaba. config_wrapper: 4 líneas de código propio y 12 puntos de llamada para no escribir una llamada estándar de la biblioteca; la capa no oculta nada. reports/: la lista de formatos está escrita en 3 archivos; agregar XLSX el año pasado tomó día y medio y la mitad fue encontrar los tres sitios; el esqueleto —cargar, ordenar, escribir— está copiado 3 veces.

Qué se gana y qué se pierde. config_wrapper: un concepto menos y un salto menos, en 12 lugares. Se pierde el punto único donde algún día podría leerse un almacén de secretos; si eso llega, se reintroduce con una función que haga algo —convertir tipos, fallar si falta la clave— y con ese nombre. reports/: agregar un formato pasa de 3 archivos a 1, y la lista se puede consultar en vez de repetirse. Se pierde un salto al leer.

Cómo se hizo. Siete commits, todos en verde. Pruebas de caracterización antes de tocar nada, comparando los archivos generados con salidas de referencia capturadas del código actual. El comportamiento observable es idéntico, byte a byte.

Cuándo lo revertiría. config_wrapper: si apareciera un almacén de secretos o hiciera falta validar la ausencia de una clave. reports/: si nos quedáramos con un solo formato.

Lo que encontré y no toqué. utils/dates.py (140 líneas, ilegible; 2 commits en 3 años, 0 incidentes, nadie lo lee; nota y disparadores en la cabecera del archivo).

Qué esperar de este ejemplo. Cuatro cosas.

Primera: el documento es más largo que el código y esa proporción es correcta. Los tres diffs suman unas cien líneas; el documento son seis párrafos. Lo que se evalúa es el criterio, y el criterio vive en el documento. En el proyecto real la proporción va a ser parecida.

Segunda: mira cómo está escrita cada sección. "Qué costaba" son números: cuatro líneas, doce puntos de llamada, tres archivos, día y medio, tres copias. Ni un adjetivo. "Cuándo lo revertiría" es verificable: alguien puede abrir el roadmap y contestar sí o no el lunes.

Tercera: las dos direcciones aparecen en el mismo documento, sin contradicción. A config_wrapper se le quita una capa porque no oculta nada; a reports/ se le agrega una porque concentra conocimiento repetido. La regla es una sola aplicada dos veces, y decirlo explícitamente en el documento es lo que demuestra que no estás siguiendo una moda.

Cuarta: la última sección es la más barata y la que más confianza genera. "Lo que encontré y no toqué" son dos líneas y demuestra que revisaste el perímetro completo y elegiste, en vez de haber tocado lo primero que te incomodó.

El código de partida

Ahora los rincones reales.

Rincón 1 — plugins/, el sobre-patronado

Los cinco archivos los conoces del módulo 2: base.py (la interfaz SeatingPlugin con cinco métodos abstractos), registry.py (el registro con decorador y el descubrimiento por pkgutil/importlib), config.py (la lectura de event.seating_plugin), impls/default_seating.py (la única implementación, cuyo supports() devuelve True siempre) y el __init__.py que dispara el descubrimiento. 183 líneas, de las cuales 17 hacen trabajo real.

Lo que importa aquí, y lo que hace este proyecto distinto de copiar la lección 6 del módulo 2, son los cinco usuarios:

# Usuario 1 — checkout/checkout.py
from plugins.registry import get_for as get_seating_plugin

def checkout(order):
    ...
    plugin = get_seating_plugin(event)
    for ticket in tickets:
        ticket.seat = plugin.assign_seat(order, ticket)
# Usuario 2 — api/routes.py
from plugins.registry import get_for as get_seating_plugin, SeatingPluginNotFound

@app.get("/events/<event_id>/seat-map")
def seat_map(event_id):
    event = load_event(event_id)
    try:
        plugin = get_seating_plugin(event)
    except SeatingPluginNotFound:
        return {"error": "Este evento no tiene mapa de asientos"}, 404
    return plugin.render_map(event)
# Usuario 3 — admin/commands.py
def release_expired_reservations():
    """Libera los asientos de reservas vencidas. Corre cada 10 minutos."""
    for ticket in find_expired_reserved_tickets():
        get_seating_plugin(load_event(ticket.event_id)).release_seat(ticket)
        ticket.status = "available"
        save(ticket)
# Usuario 4 — admin/panel.py
from plugins.config import DEFAULT_PLUGIN
from plugins.registry import _REGISTRY, discover

def seating_plugin_choices():
    """Opciones del desplegable 'motor de asientos' del formulario de evento."""
    discover()
    return [(name, name.title()) for name in _REGISTRY] or [(DEFAULT_PLUGIN, "Default")]
# Usuario 5 — tests/test_plugin_registry.py    (67 líneas, 4 tests)
def test_register_adds_to_the_registry(): ...
def test_discover_imports_every_module(): ...
def test_get_for_returns_the_configured_plugin(): ...
def test_get_for_raises_when_nothing_supports_the_event():
    # Registra a mano un plugin falso cuyo supports() devuelve False,
    # para poder llegar a la excepción.
    ...

Detente en el usuario 4 y en el cuarto test.

El panel importa _REGISTRY, una variable privada del módulo, para llenar un desplegable que tiene exactamente una opción. Si borras el registro, el formulario de creación de eventos deja de funcionar, y el fallo aparece días después cuando alguien de operaciones intente publicar un evento. Una búsqueda por get_for no lo encuentra; solo lo encuentra una búsqueda por módulo.

El cuarto test registra a mano un plugin falso para poder llegar al raise SeatingPluginNotFound. Piensa en lo que eso significa: el único lugar del sistema donde esa excepción se lanza es el test que la prueba. En producción es inalcanzable, porque la única implementación devuelve True siempre. Y de ahí se desprende una pregunta que tienes que contestar con evidencia: ¿el 404 del usuario 2 se ha devuelto alguna vez?

Los datos que ya conoces: cero implementaciones agregadas en dos años; tres commits, todos de mantenimiento; events.seating_plugin vale "default" en todos los eventos publicados; y un incidente de cuarenta minutos con el checkout caído porque alguien dejó un archivo a medias en impls/ y el descubrimiento lo importó.

Rincón 2 — checkout/checkout.py, el sub-estructurado

Unas trescientas líneas. Este es el fragmento con los dos ejes:

# Archivo: checkout/checkout.py   (fragmento representativo)

def checkout(order):
    tickets = [repository.get_ticket(tid) for tid in order.ticket_ids]
    customer = repository.get_customer(order.customer_id)
    event = repository.get_event(tickets[0].event_id)
    now = utcnow()

    # ---- 1. Precio: un if por cada tipo de boleto -------------------
    subtotal = 0.0
    for ticket in tickets:
        if ticket.kind == "general":
            price = ticket.base_price
            # Compra grupal: a partir de 10 boletos, 5% de descuento.
            if order.quantity >= 10:
                price = price * 0.95
            subtotal += round(price + price * SERVICE_FEE_RATE, 2)

        elif ticket.kind == "vip":
            # 35% sobre el base, más el cargo fijo del lounge.
            price = ticket.base_price * 1.35 + 150.0
            if customer.is_member:
                price = price * 0.90
            subtotal += round(price + price * SERVICE_FEE_RATE, 2)

        elif ticket.kind == "early_bird":
            cutoff = parse_date(event.early_bird_cutoff)
            # Después del corte el early-bird cuesta como el general: así el
            # organizador no tiene que despublicar boletos cuando pasa la fecha.
            price = ticket.base_price * 0.70 if now <= cutoff else ticket.base_price
            subtotal += round(price + price * SERVICE_FEE_RATE, 2)

        elif ticket.kind == "courtesy":
            if count_courtesies(ticket.event_id) >= COURTESY_LIMIT_PER_EVENT:
                raise TooManyCourtesies(ticket.event_id)
            subtotal += 0.0        # sin cargo por servicio: una cortesía es cortesía

        else:
            raise ValueError(f"Tipo de boleto desconocido: {ticket.kind}")

    order.total = subtotal

    # ---- 2. Cobro: un if por cada proveedor de pago -----------------
    if order.provider == "stripe":
        client = StripeClient(api_key=settings.STRIPE_KEY)
        result = client.create_charge(amount=int(round(order.total * 100)), currency="MXN")
        order.external_id = result["id"]
        order.status = "paid"

    elif order.provider == "mercadopago":
        client = MercadoPagoClient(token=settings.MP_TOKEN)
        result = client.pay(order.total, description=f"Boletia #{order.id}")
        order.external_id = result.payment_id
        order.status = "paid"

        # No quitar.
        if order.total > 10000:
            time.sleep(2)
            check = client.get_payment(result.payment_id)
            if check.status != "approved":
                order.status = "pending"

    elif order.provider == "cash":
        order.reference = generate_cash_reference(order.id)
        order.status = "pending"

    else:
        raise ValueError(f"Proveedor desconocido: {order.provider}")

    # ---- 3. Asientos ------------------------------------------------
    plugin = get_seating_plugin(event)
    for ticket in tickets:
        ticket.seat = plugin.assign_seat(order, ticket)

    # ---- 4. Avisos y analítica ----  (otras ~180 líneas)
    ...

Dos cosas de este fragmento que van a decidir tu trabajo.

El if por proveedor está repetido en cuatro archivos: aquí, en admin/refunds.py, en reports/reconciliation.py y en api/routes.py. Ese es el problema real, y es la unidad con la que se justifica el trabajo.

Y el bloque marcado "No quitar." es una cerca de Chesterton. Investígalo con los movimientos de la lección 2 antes de tocarlo. La respuesta está en el historial y cambia lo que tienes que hacer con él: no se borra, se muda —y de paso se documenta, se le pone nombre al número mágico y se le escribe la prueba que nunca tuvo—.

Rincón 3 — los candidatos a no tocar

El perímetro también incluye cosas que probablemente no hay que tocar, y encontrarlas es parte del encargo:

  • utils/dates.py: 140 líneas de parseo a mano de fechas y zonas horarias. 2 commits en 3 años, 0 incidentes.
  • El except Exception: pass alrededor de la llamada a analítica en el checkout, agregado en el mismo commit que un incidente titulado "El checkout se cayó porque el proveedor de métricas estaba caído".
  • Los tres if sobre Ticket.kind que hay en el reporte de ventas por tipo.

Tu trabajo incluye decidir sobre los tres y decir por qué.

El encargo, paso a paso

Paso 0 — Levanta la evidencia de los dos rincones.

Antes de escribir código, reúne los números con los que vas a abrir tu documento.

# Rincón sobre-patronado
find boletia/plugins -name '*.py' | xargs wc -l
grep -rn "plugins\." boletia/ --include='*.py'          # POR MÓDULO, no por función
git log --diff-filter=A --name-only -- 'boletia/plugins/impls/*'

# Rincón sub-estructurado
grep -rn '"stripe"\|"mercadopago"\|"cash"' --include='*.py' .
grep -rn 'STRIPE_KEY\|MP_TOKEN' --include='*.py' .
grep -rn '\.provider\|\.kind' --include='*.py' .
git show <commit-que-agrego-el-ultimo-proveedor> --stat

# Priorización, para el rincón que decidas no tocar
git log --since="1 year ago" --name-only --pretty=format: \
  | sort | uniq -c | sort -rn | head -20

Y lo que no está en el repositorio: cuántos valores distintos tiene events.seating_plugin, cuántas veces se devolvió el 404 del mapa de asientos, y qué incidentes hay registrados en los dos rincones. En este proyecto usa los datos que te dio la guía; en tu trabajo, ve a buscarlos.

Paso 1 — Investiga las cercas.

Al menos dos: el bloque "No quitar." del cobro con MercadoPago y el except Exception: pass de analítica. Movimientos 3, 4 y 5 de la lección 2 —git log -S, git blame, el ticket—. El resultado de esta investigación cambia el plan, así que va antes de escribir la primera prueba.

Paso 2 — Escribe las redes.

Pruebas de caracterización para los dos rincones, sobre el comportamiento observable, sin mencionar plugins, registro, interfaces ni fábricas.

Para plugins/: los tres verbos que el mecanismo expone —asignar, liberar, pintar el mapa—, porque hay tres usuarios distintos y cada uno usa uno. Para el checkout: los cuatro tipos de boleto con su aritmética, los tres proveedores con sus particularidades, y el comportamiento que descubriste en el paso 1.

Este es el paso que más gente se salta y el que más determina el resultado. Sin él no estás refactorizando: estás reescribiendo con esperanza.

Paso 3 — Dirección "quitar": desmonta plugins/.

De afuera hacia adentro, un usuario por cambio. Empieza por el de menos riesgo —la tarea programada— y deja el checkout para cuando ya hayas hecho dos. El usuario 4, el panel, no se resuelve como los otros tres: no llama a get_for, lee el registro. Ahí tienes que decidir, y la decisión va en el documento.

Después borra el andamiaje, colapsa la jerarquía y convierte en funciones. Y no sobre-corrijas: si al terminar sigue existiendo un concepto privado del proyecto para asignar asientos, vuelve atrás.

Paso 4 — Dirección "agregar": estructura el checkout.

Los dos ejes, en dos trabajos separados. Cada uno con los cuatro tiempos de la lección 3: confirmar el eje, extraer sin diseñar, leer la tabla de firmas, y recién entonces escribir el contrato y el punto de elección.

Del lado de los proveedores, el trabajo no termina en el checkout: hay cuatro archivos con el mismo if, y dejar tres sin migrar es peor que no haber empezado.

Paso 5 — Decide sobre lo que no vas a tocar.

Los tres candidatos del rincón 3, más lo que encuentres tú. Tres preguntas por cada uno, decisión, y nota con disparadores en el archivo.

Paso 6 — Escribe el documento.

Las cinco secciones más la lista de lo que no tocaste.

Qué se entrega

Cinco cosas.

Uno: el código, antes y después. Un diff o las dos versiones. Con el comportamiento observable idéntico, demostrado por las pruebas del paso 2.

Dos: la tabla de métricas, con números. Una fila por rincón y una columna por unidad: archivos, líneas, líneas de trabajo real, saltos para responder una pregunta concreta, archivos que hay que tocar para agregar un caso, conceptos privados del proyecto, pruebas del andamiaje, pruebas de comportamiento, implementaciones reales e incidentes atribuibles. Antes y después.

Tres: el documento de justificación, con las cinco secciones de la lección 6:

  • Contexto, en dos líneas. Sin esto, nada de lo demás se puede evaluar.
  • Qué costaba. Números, no adjetivos, para los dos rincones.
  • Qué se gana y qué se pierde. Las dos, con el mismo tono. Del lado de plugins/, di que quitar el registro acopla el checkout a la función de asientos; del lado del checkout, di que agregaste un salto y un concepto nuevo. Decirlo tú es lo que te vuelve creíble.
  • Cómo se hizo. La bitácora, con la frase clave: las pruebas se escribieron antes y no cambiaron.
  • Bajo qué condición se revertiría cada decisión. Verificable el lunes, y en dos niveles para plugins/: qué haría falta para volver a abstraer, y qué haría falta —bastante más— para volver a un mecanismo de carga dinámica.

Cuatro: la bitácora. Tus commits en orden y qué verificaste después de cada uno. Es corta y demuestra la propiedad más importante del método: que en ningún momento el sistema estuvo roto.

Cinco: la lista de lo que no tocaste. Con las tres respuestas y el disparador de cada uno.

Y una restricción de tamaño, que es parte del ejercicio: el documento entero no debe pasar de dos páginas. Si no cabe, es que estás explicando lo que hiciste en vez de por qué.

Cómo se evalúa

No hay porcentajes. Hay ocho cosas y las ocho son binarias.

El comportamiento observable no cambió, y lo demuestras con pruebas escritas antes de tocar nada. Una prueba escrita después prueba lo que hiciste, no lo que había.

Cada paso dejó el sistema funcionando. Se ve en la bitácora. Si hay un commit intermedio que no arranca o no pasa las pruebas, el método no se siguió.

Encontraste los cinco usuarios de plugins/ —no solo el del checkout— y resolviste el cuarto con una decisión justificada, no ignorándolo.

Encontraste los cuatro archivos con el if de proveedores, y los migraste los cuatro.

Investigaste las cercas antes de tocarlas. Si el comportamiento del antifraude de MercadoPago desapareció en tu refactor, eso es un fallo aunque todas las pruebas pasen —porque significa que las pruebas no lo cubrían y tú no lo investigaste—.

No borraste la columna en el mismo cambio. Si tu entrega incluye una migración destructiva junto con la eliminación de código, es un fallo aunque todo lo demás esté perfecto. El código es reversible; los datos no.

La justificación usa números y tiene condición de reversión verificable. Si tu documento dice "estaba sobre-diseñado" sin decir 183 líneas, cero implementaciones en dos años y seis saltos, no es una justificación: es una opinión. Y "si en el futuro hay más plugins" no es una condición: no se puede contestar.

No sobre-corregiste. Este es el que más gente falla y merece su párrafo aparte.

Cuando quitas una abstracción hay un impulso fuerte de dejar algo en su lugar, porque entregar un cambio que solo borra se siente incorrecto. Así aparecen desmontajes que reemplazan el registro de plugins por una fábrica, o por una clase SeatingService con tres métodos, o por "una interfaz más simple, de dos métodos en vez de cinco". Todas son la misma abstracción con menos calorías y ninguna resuelve nada: siguen teniendo una implementación. La respuesta correcta a "una abstracción con un solo implementador" no es "una abstracción más pequeña con un solo implementador". Es una función.

Y la simétrica, del otro rincón: no sobre-estructures. Si tu solución para los tipos de boleto incluye un registro dinámico, configuración externa o descubrimiento de módulos, subiste cuatro peldaños de más. Las cuatro reglas las escribe tu equipo, en tu repositorio, y la lista cabe en cinco líneas visibles.

Errores comunes

Empezar por borrar (de método). Qué pasa: alguien abre plugins/base.py, ve la interfaz de cinco métodos que nadie necesita, y la borra. En ese instante se rompen la implementación, el registro, el checkout, el endpoint del mapa, la tarea programada, el panel y los cuatro tests. Pasa dos horas apagando incendios sin poder correr nada, y termina revirtiendo con la sensación de que el rincón era intocable —lo cual refuerza exactamente la creencia que lo mantuvo dos años—. Por qué pasa: la interfaz es lo que se siente como el problema, así que es lo primero que uno quiere quitar; pero es la base de la pirámide, no la punta. Cómo detectarlo: si después de tu primer commit no puedes correr las pruebas, empezaste por el lugar equivocado. Cómo corregirlo: de afuera hacia adentro, y borra solo lo que ya no aparece en ningún grep. La búsqueda es tu permiso para borrar.

Hacer las dos direcciones en el mismo cambio (de alcance). Qué pasa: alguien entrega un solo diff enorme que desmonta plugins/, mete Strategy en los precios, mete Factory en los proveedores y de paso arregla dos cosas más. Es imposible de revisar, imposible de revertir por partes, y si algo falla en producción hay mil líneas de sospechosos. Y hay un daño adicional menos visible: los dos rincones tienen riesgos distintos —uno borra código del camino del checkout, el otro mueve el cálculo del dinero— y mezclarlos impide desplegarlos con cautelas distintas. Por qué pasa: el encargo llegó junto, así que se siente como un solo trabajo. Cómo detectarlo: si tu bitácora tiene menos de seis entradas para este proyecto, es demasiado grueso. Cómo corregirlo: tres trabajos separados como mínimo —el desmontaje, el eje de precios, el eje de proveedores— y dentro de cada uno, un archivo por commit. Los tres pueden vivir en la misma entrega y en el mismo documento; lo que no pueden es vivir en el mismo diff.

Entregar el código sin el documento, o el documento con adjetivos (de comunicación). Qué pasa: alguien hace un trabajo impecable y lo entrega con una descripción de una línea: "limpieza de plugins y refactor del checkout". Quien revisa no tiene con qué evaluar si fue buena idea, así que evalúa lo único que puede sin contexto: el riesgo. Y un cambio que toca el corazón del sistema, sin justificación, es riesgo alto por definición. Se rechaza, o se queda meses sin revisar. Por qué pasa: el trabajo se siente terminado cuando el código funciona. Cómo detectarlo: si tu descripción no contiene ni un número ni una condición verificable, no está lista. Cómo corregirlo: escribe el documento mientras trabajas, no al final. Anota cada decisión y cada alternativa que descartaste en el momento en que la tomas; escribirlo después, de memoria, es lo que lo vuelve burocrático y lo que hace que se pierdan las mejores razones.

Ejercicios

Ejercicio 1 — Revisa una entrega ajena. Un compañero entrega este trabajo. Encuentra los cuatro fallos y di qué se rompió o pudo romperse en cada uno.

commit 1  Quita la interfaz SeatingPlugin y el registro de plugins
          (borra base.py, registry.py, config.py, impls/, tests/test_plugin_registry.py)
commit 2  Arregla los imports rotos en checkout, routes y commands
commit 3  Crea SeatingService con los métodos assign, release y render
commit 4  Refactor completo del checkout: Strategy de precios + Factory de pagos
commit 5  Migración: elimina la columna events.seating_plugin
commit 6  Agrega tests

Documento entregado: "Limpieza del módulo de plugins y refactorización del checkout aplicando Strategy y Factory. El código queda mucho más mantenible y respeta el principio abierto-cerrado."

Ver solución

Fallo 1 — el orden está invertido y no hubo red (commits 1, 2 y 6). Borró primero y arregló después: entre el commit 1 y el 2 el sistema no arranca. Y las pruebas están al final, así que en ningún momento hubo red; el commit 6 prueba lo que él escribió, no lo que había. Si el comportamiento cambió por el camino —por ejemplo, si el orden de asignación de asientos dejó de ser alfabético—, las pruebas nuevas lo consagran como correcto.

Además falta el panel: en ningún commit aparece admin/panel.py. Como importaba _REGISTRY, el formulario de creación de eventos está roto desde el commit 1 y nadie lo sabe todavía.

Fallo 2 — sobre-corrección (commit 3). SeatingService con tres métodos es la misma abstracción, más pequeña: sigue habiendo un concepto privado del proyecto, sigue habiendo un salto, y la clase no tiene estado —es un espacio de nombres disfrazado—. La respuesta correcta era seating/seating.py con tres funciones.

Fallo 3 — el commit 4 es un monolito. "Refactor completo del checkout" con dos ejes distintos y cuatro archivos, todo junto. Imposible de revisar, imposible de revertir por partes. Y hay un riesgo específico: en un cambio de ese tamaño, el bloque del antifraude de MercadoPago tiene todas las papeletas de desaparecer sin que nadie lo note, porque no había ninguna prueba que lo cubriera. Ese sería el fallo más caro de toda la entrega y no se vería hasta el siguiente cierre de mes.

Fallo 4 — la migración destructiva (commit 5), y el documento (todo). La columna se borra en el mismo trabajo y antes de confirmar que nadie la lee. Si el reporte de operaciones la consultaba, el dato ya no existe y revertir el despliegue no lo trae de vuelta.

Y el documento no es un documento: dos nombres de patrón, un principio invocado sin nombrar el eje, y dos adjetivos —"mucho más mantenible"—. No hay un solo número, no hay costo aceptado, y no hay condición de reversión. Quien revise no tiene con qué decidir salvo el riesgo, y el riesgo es alto.

Por qué funciona: los cuatro fallos son los errores comunes del módulo en su forma más pura, y los cuatro son invisibles si solo miras el código final —que probablemente esté bien—. El método deja rastro en la bitácora y en el documento, no en el resultado. Si los encontraste sin mirar la solución, tu propia entrega va a salir bien.

Ejercicio 2 — La variante que cambia la respuesta. Rehaz el análisis de plugins/ suponiendo que existe este documento: un contrato firmado con el Teatro Metropolitan, con entrega en cuatro meses, que obliga a Boletia a permitir que el recinto conecte su propio sistema de butacas, desplegado por ellos, sin que Boletia publique una versión nueva. ¿Qué cambia y qué harías?

Ver solución

Cambia el dato que sostenía todo el diagnóstico. "Cero implementaciones en dos años" era la evidencia central, y ahora hay una segunda implementación con fecha y con firma. Ya no es una predicción: es una obligación. La regla de tres tiene una excepción explícita para esto —cuando el caso siguiente está comprometido, no imaginado— y aquí se cumple con documento.

Cambian también las tres preguntas de la lección 7, y las tres pasan a "sí": se va a tocar, bloquea una entrega comprometida, y va a leerlo gente de fuera del equipo.

Qué haría: no desmontar. Pero tampoco dejarlo como está, y esta es la parte que distingue una buena respuesta de una obvia. El mecanismo fue diseñado para un contexto imaginado y ahora hay uno real, así que hay que revisarlo contra el requisito verdadero:

  • La interfaz de cinco métodos se diseñó sin ningún consumidor. Ahora hay uno concreto que puede decir qué necesita. Muy probablemente sobren métodos y falte alguno —por ejemplo, reservar temporalmente mientras el cliente paga—.
  • El supports() que devuelve True siempre se vuelve peligroso de verdad: con dos plugins registrados, quién gana lo decide el orden de iteración de un diccionario. Eso se resuelve antes, no después.
  • El descubrimiento por carpeta es exactamente el mecanismo que causó los cuarenta minutos de caída. Con código de terceros de por medio el riesgo se multiplica: ahora el archivo que revienta al importar lo escribió alguien que no está en tu turno de guardia. Hay que aislar la carga para que el fallo de un plugin no tumbe el checkout.
  • Y la interfaz se vuelve un contrato con versión: hay que decidir qué pasa cuando la cambies y el Teatro no actualice.

Y lo que no cambia: el diagnóstico del checkout es idéntico. Ese rincón sigue necesitando estructura, con la misma evidencia y en la misma dirección. Tu entrega seguiría teniendo las dos direcciones; lo que cambia es cuál rincón recibe cuál.

La conclusión que quiero que veas: el mismo código, con un documento firmado encima, pasa de "quítalo" a "consérvalo y arréglalo". Y las dos respuestas son correctas, cada una en su contexto. Por eso la primera sección de tu documento es el contexto: sin él, nadie puede evaluar el resto, ni siquiera tú.

Por qué funciona: este ejercicio te protege del riesgo más real de toda la guía, que es salir con un reflejo antiabstracción. El criterio no es "quitar": es leer el contexto y decidir.

Ejercicio 3 — La sección que casi nadie escribe. Escribe la sección "Lo que encontré y no toqué" de tu entrega, con los tres candidatos del rincón 3. Máximo seis líneas en total, y cada uno con su razón y su disparador.

Ver solución

Una versión que funciona:

Lo que encontré y no toqué.

utils/dates.py — 140 líneas de parseo a mano de cuatro formatos, con aritmética de zonas horarias escrita a mano. 2 commits en 3 años, 0 incidentes, ningún trabajo pendiente lo toca y nadie lo lee (se usa a través de parse_date). Nota y disparadores en la cabecera del archivo: se vuelve prioridad si vendemos en un segundo huso horario, si aparece cualquier incidente de fechas, o si hay que agregarle un formato.

El except Exception: pass de analítica en el checkout — es una cerca del #1904: analítica caída no debe tumbar una venta, y esa decisión sigue siendo correcta. Lo único que cambié es que ahora registra el fallo antes de continuar; antes se lo tragaba en silencio, así que nadie se enteraba de que las métricas se estaban perdiendo. El comportamiento observable para el cliente es idéntico.

Los tres if sobre Ticket.kind del reporte de ventas por tipo — son el mismo eje que acabo de estructurar, pero ese reporte no consume calculate_line_price: agrupa por tipo para mostrar totales. Se resuelven solos cuando Ticket.kind deje de ser texto libre y pase a ser un enum; ese trabajo toca datos, necesita su propio plan de migración y está en el ticket #3402.

Tres decisiones de redacción que vale la pena señalar:

El primero se deja entero y se documenta. La nota vive en el archivo, no solo en la entrega, porque la próxima persona que lo abra dentro de un año no va a leer tu documento: va a leer el archivo.

El segundo se toca un poco, y se dice exactamente cuánto. "Ahora registra el fallo" es un cambio, y decir en la misma frase que el comportamiento observable no cambió es lo que evita que quien revise se preocupe. Es el resultado típico de investigar una cerca del tipo 1: no la quitas, la vuelves legible.

El tercero se explica con su razón técnica, no con "es mucho trabajo". Decir por qué ese if no desaparece con tu refactor —porque agrupa, no calcula— demuestra que lo miraste de verdad. Y remitir al ticket que ya existe convierte tu decisión en parte de un plan y no en una omisión.

Por qué funciona: esta sección son seis líneas y es la que más confianza genera en toda la entrega, porque demuestra que revisaste el perímetro completo y elegiste, en vez de haber tocado lo primero que te incomodó. Es también la que mejor te prepara para una entrevista: la pregunta "cuéntame de una vez que decidiste no arreglar algo" separa a quien tiene criterio de quien tiene entusiasmo.

Resumen y cierre de la guía

Con este proyecto cierras la guía. Tomaste dos rincones del mismo sistema y les hiciste lo contrario: a uno le quitaste ciento ochenta y tres líneas de estructura que no ocultaba nada, al otro le agregaste la estructura que su variación real pedía. Escribiste el documento que defiende las dos con su tradeoff, con números y con condiciones de reversión verificables. Y entregaste la lista de lo que decidiste dejar en paz, que es la parte que demuestra que elegiste.

Mira hacia atrás, porque el arco importa tanto como cada pieza.

Módulo 1 — Qué son de verdad los patrones. Instaló la definición honesta: un patrón es una solución con nombre a un problema que se repite, descubierta y no inventada. Y su valor principal, el que se cobra todos los días: vocabulario. Sales de ahí sabiendo nombrar estructuras en código ajeno y distinguir el puñado que importa del ruido del catálogo.

Módulo 2 — Cuándo NO abstraer. El freno, puesto a propósito antes de enseñar un solo patrón concreto. Cada abstracción se paga, la regla de tres, YAGNI, el anti-patrón del plugin para una sola implementación, y el intercambio entre acoplamiento e indirección. Sales sabiendo decir "aquí no" con argumentos, que es lo que evita fabricar el síndrome del martillo nuevo.

Módulo 3 — Comportamiento que varía. Strategy, Template Method y State, más la frontera con las funciones de primera clase y la trampa de que no todo condicional es una Strategy escondida. Sales sabiendo separar lo que cambia de lo que no, sin convertir cada if en una jerarquía.

Módulo 4 — Crear objetos. Factory, Builder, Singleton —el sospechoso— e inyección de dependencias en palabras simples. Sales sabiendo ordenar el "cómo se construye esto" sin ceremonia innecesaria, y sabiendo cuándo un constructor o una función alcanzan.

Módulo 5 — Estructurar y adaptar. Adapter, Facade, Decorator, Composite, y la línea con "solo escribe una función que envuelva". Sales sabiendo aislar una dependencia externa para que su desorden no se filtre al resto del sistema.

Módulo 6 — Comunicar entre partes. Observer, Command y el acoplamiento por evento, con su costo escondido dicho en voz alta: el flujo que ya no puedes seguir con el dedo. Sales sabiendo desacoplar quién avisa de quién escucha, sabiendo lo que pierdes.

Módulo 7 — Vocabulario de revisión. El otro lado del diccionario: code smells, God object, feature envy, shotgun surgery, anti-patrones. Sales sabiendo nombrar un problema en una revisión de forma que sea accionable y no una sentencia, y entendiendo que un patrón es una conversación, no un veredicto.

Módulo 8 — Refactorizar con criterio. Leer antes de tocar, reconocer el patrón que quiere emerger y el que hay que quitar, ejecutar en pasos que nunca dejan el sistema roto, justificar por el tradeoff y no por el nombre, y decidir cuándo no hacer nada. Sales sabiendo tomar un rincón desconocido, diagnosticarlo en cualquiera de las dos direcciones, ejecutarlo sin romper nada y defender cada decisión ante un equipo.

Si tuvieras que quedarte con una sola idea de los ocho módulos, que sea esta: la calidad no está en la forma, está en la correspondencia entre la forma y el contenido. La misma interfaz con tres implementaciones es buen diseño y con una es peso muerto. El mismo if de cuatro ramas pide estructura si el eje crece y no la pide si es una condición del momento. El mismo rincón feo hay que arreglarlo si alguien lo toca cada semana y hay que dejarlo en paz si nadie lo abre desde hace tres años. No hay reglas: hay preguntas con respuestas verificables, y ahora las tienes.

A dónde ir ahora

Las guías hermanas no son "más de lo mismo": cada una resuelve un problema distinto que probablemente sentiste durante esta. Ve a la que te haga falta.

Si durante esta guía hubo frases que te sonaron a que les faltaba un piso —acoplamiento, cohesión, responsabilidad única, descomposición, tradeoffs— ese piso es software-development-foundations-guide. Los patrones son, en buena medida, nombres para formas concretas de aplicar esos principios: Strategy es una manera específica de bajar el acoplamiento entre quien decide y quien ejecuta; Facade es una manera específica de subir la cohesión de una interfaz. Con esa base debajo, todo lo de aquí se vuelve más sólido.

Si sabes qué decir en una revisión pero no cómo decirlo sin generar fricción —o cómo escribir el PR, cómo recibir una crítica, cuándo bloquear un cambio y cuándo aprobarlo con un comentario— eso es clean-code-and-code-review-guide. Esta guía te dio las palabras; aquella te da la conversación. Son complementarias por diseño, y el módulo 7 y la lección 6 de este viven justo en la frontera.

Si la parte que más te costó de este módulo fue la red de seguridad —escribir las pruebas de caracterización, sustituir una API externa sin volver la prueba frágil, o hacer testeable un código que no lo es— eso es testing-backend-applications-guide. Y es, con diferencia, la que más rendimiento te va a dar después de esta: sin red no se puede refactorizar, y todo lo que aprendiste aquí depende de tenerla.

Si te llamó la atención cuánto de tu diagnóstico salió del historial y no del códigogit log -S para encontrar el commit que trajo una línea, git blame para saber quién la tocó, el conteo de archivos más modificados para priorizar, el revert que te dijo que una cerca era del tipo 1— eso es git-github-guide. Aprender a leer la historia de un repositorio es una de las habilidades peor distribuidas del oficio y una de las que más rápido te distingue en un equipo nuevo.

Y si te quedaste con la pregunta de qué pasa cuando estas mismas decisiones ocurren entre programas y no dentro de uno —cuándo partir un servicio, cuándo comunicar por una cola, dónde poner un límite— ese es el piso de arriba, la guía de arquitectura y diseño de sistemas. Lleva contigo el criterio que construiste aquí, porque es el mismo con costos mucho más caros: quien no sabe decir "no" a una interfaz innecesaria tampoco sabe decir "no" a un microservicio innecesario.

Cierro con lo que decía la promesa al principio. No saliste con veintitrés patrones memorizados. Saliste con dos cosas más raras y más valiosas: vocabulario para nombrar lo que ves en código ajeno, y criterio para juzgar si esa estructura se gana su lugar —en las dos direcciones, y sabiendo también cuándo la respuesta correcta es no tocar nada—. Eso es lo que se defiende en una entrevista y, más importante, lo que se nota en un equipo la primera semana.

Recursos