Módulo 8: Refactorizar con criterio (capstone)

1. Presentación del módulo: el patrón correcto en el momento correcto

Descripción

Al terminar esta lección vas a tener tres cosas. Primero, la pregunta única que gobierna todo este módulo y que resume las siete lecciones anteriores en una sola línea: ¿la estructura que hay corresponde a la variación que hay? Segundo, vas a ver los dos rincones de Boletia sobre los que vas a trabajar hasta el final de la guía —plugins/, que tiene estructura de sobra, y checkout/checkout.py, que no tiene la que necesita— con sus números encima de la mesa, para que el diagnóstico no dependa de tu gusto. Y tercero, vas a entender por qué el mismo alumno tiene que resolver los dos en direcciones opuestas, y por qué eso —no la cantidad de patrones que sepas nombrar— es lo que se evalúa en el proyecto final.

Esto importa por una razón muy concreta. Después de siete módulos ya sabes nombrar estructuras, sabes cuáles se ganan su lugar y cuáles no, y sabes decirlo en una revisión de código. Pero todo eso lo hiciste sobre ejemplos que alguien te presentó ya clasificados: "aquí hay una Strategy", "este rincón sobra". En el trabajo real nadie te dice cuál es cuál. Llega un encargo de un martes cualquiera, abres un archivo que no escribiste, y tienes que decidir tú solo si a ese código le falta estructura, le sobra, o —esta es la tercera opción que casi nadie enseña— está feo pero hay que dejarlo en paz porque tocarlo cuesta más de lo que rinde.

Ese "decidir tú solo" tiene una parte técnica y una parte de comunicación, y las dos se aprenden aquí. La técnica es el diagnóstico y el procedimiento: leer antes de tocar, reconocer qué patrón quiere emerger o cuál hay que desmontar, y moverse en pasos que nunca dejan el sistema roto. La de comunicación es la que más gente subestima: una refactorización correcta que no puedes defender no se aprueba, y una que se aprueba sin defensa deja al equipo sin saber por qué el código cambió. Por eso el proyecto final pide dos entregables, código y documento, y por eso el documento pesa tanto como el diff.

Conexión con el módulo: esta lección instala el marco y el terreno; todavía no vas a refactorizar nada. La lección 2 te da la regla que evita los desastres —entender qué hace un código y por qué está así antes de mejorarlo— con la ley de la cerca de Chesterton contada en cristiano y aplicada a una línea rarísima del checkout de Boletia. La lección 3 te enseña a reconocer que un código está pidiendo estructura, y la diferencia entre imponer un patrón y dejar que emerja. La lección 4 hace el movimiento inverso, el que casi nadie enseña: reconocer la estructura que hay que quitar y desmontarla sin miedo. La lección 5 es la disciplina que hace posible las dos: pasos chicos, comportamiento intacto en cada uno, capacidad de parar a la mitad. La lección 6 te enseña a justificar el cambio por su tradeoff y no por el nombre del patrón. La lección 7 —la más madura de la guía— te da el criterio para no tocar nada. Y la lección 8 es el proyecto final: los dos rincones, las dos direcciones, y el documento que los defiende.

Un plano no dice si la casa está bien

Imagina que te contratan para revisar una casa que lleva ocho años habitada. No la diseñaste tú. Entras con un cuaderno y en dos horas anotas cosas como estas:

  • El baño de visitas tiene tres llaves de paso independientes: una para el lavabo, otra para el inodoro y otra general. En una casa de una sola planta con un solo baño de visitas, eso son dos llaves de más. Alguien las instaló pensando en algo que no ocurrió.
  • La cocina no tiene ninguna. Para cambiar el empaque de la llave del fregadero hay que cerrar el agua de la casa entera, avisarle a todo el mundo, y trabajar contra reloj.
  • Y en el cuarto del fondo hay un cable que sale de la pared, da la vuelta por el techo y vuelve a entrar dos metros más allá. Se ve horrible. Lleva ocho años ahí. Nadie sabe por qué está.

Fíjate en tres cosas que ese cuaderno deja claras y que son exactamente el módulo entero.

Primera: los dos primeros problemas son el mismo problema. No son "demasiada plomería" y "poca plomería" como si fueran vicios distintos. Son la misma pregunta contestada mal en dos direcciones: ¿cuánto control necesitas sobre esta parte, según cuántas veces vas a tener que intervenirla? En el baño de visitas, que se toca cada cinco años, la respuesta era "poco" y pusieron mucho. En la cocina, que se toca cada seis meses, la respuesta era "bastante" y no pusieron nada. Un buen revisor no tiene dos criterios: tiene uno, la proporción entre estructura y necesidad real, y lo aplica en las dos direcciones.

Segunda: el tercer problema no se parece a los otros dos. El cable feo del cuarto del fondo puede ser un desastre esperando a ocurrir o puede ser la solución perfectamente razonable a algo que ya no está a la vista —una viga que no se podía perforar, una humedad que hubo en el 2019—. Y hay un dato más importante que su fealdad: nadie lo toca. Arreglarlo cuesta romper pared, y el beneficio es estético. Ese cable es la lección 7.

Tercera, y esta es la que cuesta aceptar: nada de eso se decide mirando el plano. El plano te dice qué hay. No te dice cuántas veces al año alguien abre esa pared, ni por qué se puso ese cable, ni qué le pasó a la casa en el 2019. Todas las decisiones importantes de este módulo se toman con información que no está en el código: el historial, la frecuencia de cambio, los incidentes, quién más depende de esto y qué está bloqueado hoy.

Esa es la diferencia entre los módulos 3 a 6 y este. Allá aprendiste a leer el plano. Aquí aprendes a hacer la visita.

Ejemplo trabajado: los dos rincones, con la misma pregunta

Vamos al código. Aquí están los dos rincones de Boletia, uno al lado del otro, y una sola pregunta aplicada a los dos.

Rincón A — plugins/. Ya lo conoces del módulo 2. Cinco archivos, ciento ochenta y tres líneas. Esta es su forma:

# Archivo: plugins/base.py       (la interfaz: 5 métodos abstractos)
class SeatingPlugin(ABC):
    @abstractmethod
    def name(self) -> str: ...
    @abstractmethod
    def supports(self, event) -> bool: ...
    @abstractmethod
    def assign_seat(self, order, ticket) -> str | None: ...
    @abstractmethod
    def release_seat(self, ticket) -> None: ...
    @abstractmethod
    def render_map(self, event) -> dict: ...
# Archivo: plugins/registry.py   (el registro y el descubrimiento dinámico)
def discover():
    """Importa todos los módulos de plugins/impls/ para que se auto-registren."""
    import plugins.impls as impls_pkg
    for _, module_name, _ in pkgutil.iter_modules(impls_pkg.__path__):
        importlib.import_module(f"plugins.impls.{module_name}")
# Archivo: plugins/impls/default_seating.py   (la ÚNICA implementación)
@register
class DefaultSeatingPlugin(SeatingPlugin):

    def supports(self, event) -> bool:
        return True                      # ← acepta cualquier evento. Siempre.

    def assign_seat(self, order, ticket) -> str | None:
        if ticket.seat is not None:
            return ticket.seat
        row = query(
            "SELECT label FROM seats WHERE event_id = ? AND taken = 0 ORDER BY label LIMIT 1",
            ticket.event_id,
        )
        ...

Rincón B — checkout/checkout.py. Unas trescientas líneas. Este es el corazón: un fragmento representativo, con los dos condicionales que importan.

# Archivo: checkout/checkout.py   (fragmento)

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
            if order.quantity >= 10:
                price = price * 0.95
            subtotal += round(price + price * SERVICE_FEE_RATE, 2)

        elif ticket.kind == "vip":
            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)
            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"

    elif order.provider == "cash":
        order.reference = generate_cash_reference(order.id)
        order.status = "pending"       # el efectivo no cobra: genera referencia

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

    # ---- 3. Asientos, avisos, analítica ----  (otras ~180 líneas)
    ...

Ahora la pregunta, la misma para los dos: ¿la estructura que hay corresponde a la variación que hay? Contéstala con datos, no con impresiones.

plugins/checkout/checkout.py
Comportamientos distintos que existen hoy1 (DefaultSeatingPlugin)4 tipos de boleto y 3 proveedores de pago
Estructura para acomodarlosinterfaz de 5 métodos, registro, descubrimiento dinámico, columna de configuraciónninguna: dos cadenas de if dentro de la misma función
Archivos que hay que tocar para agregar uno1 (crear el archivo)4 para un proveedor; 4 para un tipo de boleto
Saltos para responder "¿cómo funciona esto?"6 saltos, 4 archivos1 salto… si logras encontrar la rama entre trescientas líneas
Veces que se agregó uno en los últimos 2 años02 proveedores y 1 tipo de boleto
Costo pagado ya1 incidente de 40 min por un archivo a medias que reventó al importarse3 incidentes de cobro por olvidar una rama al agregar un tipo de boleto

Qué esperar de esta tabla. Tres lecturas, en orden de importancia.

La primera: los dos rincones fallan la misma prueba, en direcciones opuestas. plugins/ tiene estructura para n comportamientos y hay uno. checkout tiene estructura para uno y hay siete —cuatro tipos de boleto y tres proveedores— repartidos en dos ejes distintos dentro de la misma función. Ninguno de los dos es "código malo" en abstracto: los dos son código cuya forma no corresponde a su contenido. Y el remedio de uno es exactamente el veneno del otro. Si aplicaras a checkout el reflejo de "quita abstracción" empeorarías; si aplicaras a plugins/ el reflejo de "esto pide un patrón", también.

La segunda: mira la fila del historial, porque es la que decide. Cero implementaciones en dos años convierte la discusión sobre plugins/ en un dato. Dos proveedores y un tipo de boleto agregados en el mismo período convierten la discusión sobre checkout en otro dato. Esa fila no está en el código: está en git log. Y es, casi siempre, la evidencia más fuerte que vas a poder llevar a una revisión, porque nadie discute contra el historial de su propio repositorio.

La tercera, la más incómoda: la última fila dice que los dos ya cobraron. Cuarenta minutos de checkout caído por un mecanismo que nadie usaba. Tres cobros mal calculados por un if olvidado. Cuando alguien pregunte "¿y por qué gastar tiempo en esto?", esa fila es la respuesta, y es mucho mejor respuesta que "porque está mal diseñado". El costo de no hacer nada casi siempre existe; lo que falta es haberlo contado.

La balanza: estructura contra variación

Detrás de todo el módulo hay una sola balanza con dos platos. En un plato, cuánta variación real existe —cuántos comportamientos distintos hay, con qué frecuencia aparecen nuevos, qué tan distintos son entre sí—. En el otro, cuánta estructura pusiste para acomodarla: interfaces, capas, registros, configuración, indirección. La salud de un rincón de código es que los dos platos estén parejos.

Dibuja los cuatro cuadrantes y verás el mapa completo de tu trabajo:

Poca estructuraMucha estructura
Poca variaciónSano. Código directo para un problema directo. La mayoría del código bueno vive aquí y no se le nota⚠️ Sobre-patronado. plugins/. La abstracción no se gana su lugar: se paga indirección sin comprar nada. Lección 4
Mucha variación⚠️ Sub-estructurado. checkout. Cada cambio obliga a tocar varios lugares y olvidarse de uno cuesta dinero. Lección 3Sano. payments/ y notifications/ después de los módulos 4 y 6: hay tres cosas distintas y hay una forma común

Tres observaciones sobre este cuadro que conviene que te lleves.

Las dos casillas verdes se ven muy distintas y las dos son correctas. La de arriba a la izquierda es una función de veinte líneas sin ninguna interfaz. La de abajo a la derecha es una interfaz con tres implementaciones y un punto de elección. Alguien que solo mira la forma diría que la segunda es "más profesional". No lo es: es la forma que corresponde a su contenido, igual que la primera. La calidad no está en la forma, está en la correspondencia.

Moverse entre casillas cuesta, y cuesta distinto en cada dirección. Ir de "sub-estructurado" a "sano" es agregar: extraer, definir un contrato, mover el tráfico. Es trabajo aditivo y se puede hacer con el sistema encendido, un paso por vez. Ir de "sobre-patronado" a "sano" es quitar, y quitar tiene una dificultad que agregar no tiene: hay que estar seguro de que nadie más usa lo que vas a borrar, y esa certeza se busca, no se supone. Por eso la lección 4 dedica media lección a qué revisar antes.

Y el cuadro no dice cuándo actuar. Un rincón puede estar en una casilla amarilla y aun así no valer la pena tocarlo, porque nadie lo lee, nadie lo cambia y no bloquea nada. El cuadro te dice qué le pasa a un código; la lección 7 te dice si vale la pena hacer algo al respecto. Confundir esas dos preguntas es el error más caro del módulo, y es el que produce equipos que refactorizan mucho y entregan poco.

Las dos direcciones, y por qué van juntas

Casi todos los cursos de patrones enseñan una sola dirección: aquí hay un if feo, conviértelo en Strategy. Esta guía enseña las dos, y el proyecto final te obliga a hacer las dos en la misma entrega. La razón no es simetría estética; es que el criterio no se demuestra en una sola dirección.

Piénsalo así. Si alguien solo sabe agregar estructura, su respuesta a todo es un patrón, y con el tiempo produce un plugins/ por rincón. Si alguien solo sabe quitarla, su respuesta a todo es "simplifica", y con el tiempo produce un checkout de trescientas líneas. Las dos personas tienen un reflejo, no un criterio. Un reflejo se reconoce porque da la misma respuesta antes de mirar los datos.

La forma de comprobar que lo tuyo es criterio y no reflejo es exactamente el proyecto final: dos rincones, dos direcciones opuestas, la misma persona, el mismo martes, y un documento que explique por qué cada uno recibió lo contrario que el otro. 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 hay una tercera dirección, que es la que hace adulta a esta guía: no hacer nada. En el proyecto también vas a entregar la lista de lo que decidiste no tocar, con la razón. Esa lista, para mucha gente, es lo más difícil de escribir, porque no tocar se siente como no trabajar. Es al revés: decidir no tocar algo, con evidencia y por escrito, es una decisión de ingeniería tan real como cualquier refactorización, y es la que más tiempo de equipo ahorra.

El terreno: lo que vas a tener enfrente

Para que llegues a la lección 2 con el mapa fresco, esto es todo lo que el módulo pone sobre la mesa.

El rincón sobre-patronado, plugins/. Cinco archivos, ciento ochenta y tres líneas, de las cuales diecisiete hacen trabajo real —nueve para asignar un asiento, cinco para liberarlo, tres para pintar el mapa—. Una clase base abstracta con cinco métodos, un registro con decorador, un descubrimiento dinámico que importa por nombre todo lo que encuentre en una carpeta, una columna de configuración por evento, y una sola implementación cuyo supports() devuelve True sin condiciones. Cero plugins agregados en dos años. Cinco usuarios, y uno de ellos —admin/panel.py— importa la variable privada _REGISTRY para llenar un desplegable de una sola opción. Un incidente de cuarenta minutos con el checkout caído porque alguien dejó un archivo a medias en impls/ y el descubrimiento lo importó.

El rincón sub-estructurado, checkout/checkout.py. Unas trescientas líneas que orquestan precio, cobro, asientos y avisos. Dos ejes de variación metidos en la misma función: un if por tipo de boleto (Ticket.kind es texto libre: "general", "vip", "early_bird", "courtesy") y un if por proveedor de pago (Order.provider también es texto libre: "stripe", "mercadopago", "cash"). Y el detalle que convierte esto en un problema real y no en una fealdad: el if por proveedor está repetido en cuatro o cinco archivos —el checkout, la devolución de dinero, la conciliación nocturna y la validación de la petición—, así que agregar un proveedor no es tocar un lugar, es encontrar todos.

Y las cosas que probablemente no hay que tocar. El módulo también pone delante de ti un par de rincones feos que no son prioridad, y una línea aparentemente absurda dentro del propio checkout que resulta tener una razón excelente. Los vas a conocer en las lecciones 2 y 7. Que estén ahí es deliberado: un módulo de refactorización que solo contiene cosas que hay que refactorizar entrena el reflejo, no el criterio.

Errores comunes

Llegar con el diagnóstico hecho (de método). Qué pasa: alguien lee el enunciado del proyecto —"un rincón sobre-patronado y uno sub-estructurado"— y se salta el diagnóstico, porque ya se lo dieron. Va directo a desmontar plugins/ y a meter Strategy en checkout. El resultado suele estar bien y aun así el trabajo está mal hecho, porque en el trabajo real nadie te clasifica los rincones y la habilidad que estás practicando es justamente la clasificación. Por qué pasa: la lección 1 de un módulo tiene que enseñarte el terreno, y enseñarte el terreno implica adelantarte la respuesta. Cómo detectarlo: si no puedes decir con qué evidencia concreta —cuántas implementaciones, cuántos archivos por cambio, qué dice el historial— llegaste a la conclusión, no diagnosticaste: memorizaste. Cómo corregirlo: antes de tocar nada, levanta tú los números de la tabla del ejemplo trabajado, con las búsquedas de la lección 2. Aunque ya sepas el resultado, el hábito es el entregable.

Tratar "feo" y "peligroso" como sinónimos (de criterio). Qué pasa: alguien recorre el sistema con el vocabulario recién adquirido, marca ocho rincones como problemáticos, y propone arreglarlos todos. La mitad no se ha tocado en tres años y nadie los lee. El equipo entra en un trimestre de limpieza que no desbloquea nada, y —peor— cada cambio trae su propio riesgo, así que la limpieza produce incidentes que el código feo no producía. Por qué pasa: el vocabulario nuevo hace visible lo que antes era ruido de fondo, y lo visible se siente urgente. Además, nombrar un problema produce la sensación de haberlo diagnosticado, cuando nombrar es solo la mitad. Cómo detectarlo: si tu lista de refactorizaciones no está ordenada por algo —frecuencia de cambio, cosas bloqueadas, incidentes—, no es una lista de trabajo: es un inventario de incomodidades. Cómo corregirlo: la lección 7, con sus tres preguntas. Y mientras tanto, una regla de bolsillo: antes de proponer tocar un archivo, mira cuándo fue la última vez que alguien lo tocó.

Creer que el entregable es el código (de expectativa). Qué pasa: alguien hace una refactorización impecable, la entrega con una descripción de una línea —"limpieza del módulo de plugins"— y no entiende por qué se queda tres semanas sin aprobar. Quien revisa no tiene con qué evaluar si fue buena idea, así que evalúa lo único que puede evaluar sin contexto: el riesgo. Y un cambio que toca el camino del checkout, sin justificación, es riesgo alto por definición. Por qué pasa: el trabajo se siente terminado cuando el código funciona, y escribir el porqué se siente como burocracia posterior. Cómo detectarlo: si tu descripción no contiene un solo número ni una sola condición verificable, no está lista. Cómo corregirlo: la lección 6 completa. Y desde ahora, el hábito: mientras refactorizas, anota en un archivo aparte cada decisión que tomaste y qué descartaste. Escribir el documento al final, de memoria, es lo que hace que se sienta burocrático; escribirlo mientras trabajas es casi gratis.

Ejercicios

Ejercicio 1 — Clasifica cinco rincones en el cuadro. Para cada uno, di en qué casilla de la balanza cae y con qué evidencia. Si te falta un dato para decidir, di cuál te falta: eso también es una respuesta correcta.

(a) reports/: tres exportadores —CSV, PDF, hoja de cálculo— con una interfaz común ReportExporter y un diccionario que elige por formato. Se agregó uno el año pasado. (b) utils/dates.py: ciento cuarenta líneas de parseo de fechas y zonas horarias, sin ninguna interfaz, escritas de corrido, con nombres como _fix. Dos commits en tres años, ninguno por un error. (c) notifications/: tres canales con una forma común, pero el checkout los llama a los tres por su nombre, uno tras otro, con un if por cada uno para saber si el cliente los tiene disponibles. (d) pricing/: una interfaz PricingRule, cuatro implementaciones, y un diccionario que elige por Ticket.kind. Marketing cambió las reglas cinco veces el año pasado. (e) admin/panel.py: un desplegable que ofrece "motor de asientos" con exactamente una opción, leída de un registro dinámico.

Ver solución

(a) Sano, abajo a la derecha. Tres comportamientos reales, una forma común, un punto de elección. La evidencia decisiva es "se agregó uno el año pasado": la variación no es imaginaria, ocurrió. Nota que la estructura aquí es casi idéntica en forma a la de plugins/ —una interfaz y varias implementaciones— y sin embargo el diagnóstico es opuesto. Lo que cambia no es la forma: es cuántas implementaciones reales hay.

(b) Sano, arriba a la izquierda… con una advertencia. Poca variación (nadie ha necesitado otra forma de parsear fechas) y poca estructura. La fealdad no es un cuadrante. Dos commits en tres años y ningún incidente dicen que este rincón no es un problema, es una incomodidad. Es exactamente el caso de la lección 7. Si te faltó un dato, el correcto sería: ¿alguien tiene que entenderlo pronto? —si mañana entra alguien a trabajar sobre zonas horarias, la respuesta cambia—.

(c) Sub-estructurado, abajo a la izquierda. Hay variación real (tres canales, más interesados que van a aparecer) y la estructura existe a medias: hay una forma común para los canales, pero el punto que decide a quién avisar está escrito a mano en el corazón del sistema. Es el rincón del módulo 6: agregar un cuarto interesado obliga a tocar checkout.

(d) Sano, abajo a la derecha, y con la mejor evidencia posible: cinco cambios en un año. Esa cifra es la que justifica la estructura. Sin ella, cuatro implementaciones seguirían siendo defendibles pero mucho menos obvias.

(e) Sobre-patronado, arriba a la derecha, y es un caso especialmente elocuente porque el síntoma llegó hasta la interfaz de usuario: un campo de formulario con una sola opción no es una elección, es una pregunta que no existe. Cuando una abstracción innecesaria se filtra hasta lo que ve una persona, ya no es un problema interno de diseño.

Por qué funciona: cinco rincones, y solo dos de los cinco piden acción. Esa proporción es la realista y es lo que el ejercicio quiere instalar. Un módulo de refactorización que te entrena a ver problemas en todas partes te vuelve peligroso, no útil.

Ejercicio 2 — Escribe la evidencia que te falta. Vuelve a la tabla comparativa del ejemplo trabajado. Todas sus filas salen de algún lado: unas del código, otras del historial, otras de producción. Para cada una de estas cuatro afirmaciones, escribe cómo la verificarías —el comando, la consulta o la pregunta a una persona— si llegaras hoy a Boletia sin conocer nada.

(a) "Existe una sola implementación de SeatingPlugin." (b) "Se agregaron dos proveedores de pago en los últimos dos años." (c) "La columna events.seating_plugin siempre ha valido default." (d) "El if por proveedor está repetido en cuatro o cinco archivos."

Ver solución

(a) Búsqueda por herencia y por carpeta, en todo el repositorio, incluidos los tests:

grep -rn "SeatingPlugin" --include='*.py' .
ls boletia/plugins/impls/

La parte de "incluidos los tests" no es un detalle: una segunda implementación que solo vive en tests/ cuenta como implementación real si se ejecuta. Es una de las cinco excepciones legítimas del módulo 2.

(b) El historial, filtrando por archivos agregados:

git log --diff-filter=A --name-only -- 'boletia/payments/*'
git log --since="2 years ago" --oneline -- boletia/payments/

(c) Esta no se contesta con el código: se contesta con datos de producción. Una consulta de solo lectura del tipo SELECT seating_plugin, count(*) FROM events GROUP BY 1. Y si no tienes acceso, la respuesta correcta es pedirla, no suponerla. La diferencia entre "creo que siempre vale default" y "vale default en ciento ochenta mil eventos" es la diferencia entre una opinión y un argumento.

(d) Búsqueda por el valor que actúa de llave, no por el nombre de la función, porque el conocimiento está escrito de formas distintas en cada archivo:

grep -rn '"stripe"\|"mercadopago"\|"cash"' --include='*.py' .
grep -rn '\.provider' --include='*.py' .
grep -rn 'STRIPE_KEY\|MP_TOKEN' --include='*.py' .

La tercera —buscar por las credenciales— es la que más sorpresas da, porque quien lee una clave de API casi siempre está construyendo un cliente, y a veces está en un script o una tarea programada que ninguna búsqueda por "pago" encuentra.

Por qué funciona: las cuatro afirmaciones de la tabla parecen del mismo tipo y salen de cuatro fuentes distintas —el código, el historial, la base de datos de producción y una búsqueda por valores—. Saber de dónde sale cada evidencia es literalmente el trabajo de la lección 2, y es lo que separa un diagnóstico que se puede defender de uno que se puede discutir.

Ejercicio 3 — La pregunta que decide la dirección. Un compañero te manda este mensaje: "Encontré una interfaz con un solo implementador en storage/. ¿La quito?". Escribe tu respuesta en cinco líneas como máximo. No puede ser "sí" ni "no": tiene que ser la lista mínima de cosas que necesitas saber para contestar, ordenadas por cuál preguntarías primero.

Ver solución

Una respuesta que funciona:

Antes de decidir, cuatro datos. Uno: ¿cuántas implementaciones se ejecutan de verdad, contando los tests y los otros entornos? Si hay un doble de prueba que se usa, ya son dos. Dos: ¿qué hay del otro lado de la interfaz —entrada y salida real, o lógica pura? Si hay red o disco, la frontera se justifica sola. Tres: ¿alguien fuera de este repositorio la consume? Si sí, cambiarla deja de ser un refactor. Cuatro: ¿cuántas veces se ha tocado ese rincón en el último año, y hay algo bloqueado hoy por él? Si nadie lo toca y no bloquea nada, la respuesta probablemente sea "déjalo y anótalo", aunque el olor aplique.

Fíjate en el orden y en la última. Las tres primeras preguntas deciden si el diagnóstico es correcto; la cuarta decide si vale la pena actuar, que es una pregunta distinta y que casi nadie hace. Un rincón puede estar objetivamente sobre-patronado y aun así no merecer tu semana.

Y nota lo que la respuesta no hace: no dice "quítala, las interfaces con un implementador son un anti-patrón". Esa frase es un reflejo. El módulo 2 fue muy explícito al respecto —una interfaz con un solo implementador es una pregunta, no un pecado— y este módulo agrega la segunda mitad: incluso cuando la respuesta a la pregunta es mala, todavía falta decidir si hoy es el día.

Por qué funciona: acabas de escribir, en cinco líneas, el esqueleto de las lecciones 4 y 7. Si esas cuatro preguntas se te vuelven automáticas, el proyecto final ya está medio hecho.

Resumen y siguiente paso

En esta lección instalaste la pregunta que gobierna todo el módulo: ¿la estructura que hay corresponde a la variación que hay? Viste que "demasiada estructura" y "muy poca" no son dos vicios distintos sino la misma pregunta contestada mal en dos direcciones, y que por eso el criterio se demuestra resolviendo las dos —un reflejo da la misma respuesta antes de mirar los datos; un criterio mira los datos primero—.

Conociste los dos rincones sobre los que vas a trabajar hasta el final de la guía, con sus números. plugins/: cinco archivos, ciento ochenta y tres líneas, diecisiete de trabajo real, una implementación, cero agregadas en dos años, seis saltos para responder una pregunta, cinco usuarios y un incidente de cuarenta minutos. checkout/checkout.py: trescientas líneas, dos ejes de variación en la misma función —cuatro tipos de boleto y tres proveedores—, cuatro archivos que tocar por cada cosa nueva, y el if de proveedores repetido en cuatro o cinco lugares.

Viste la balanza con sus cuatro cuadrantes y las tres conclusiones que deja: que la calidad no está en la forma sino en la correspondencia entre forma y contenido; que moverse hacia la casilla sana cuesta distinto según la dirección —agregar es aditivo, quitar exige certeza—; y que el cuadro dice qué le pasa a un código pero no si vale la pena tocarlo, que es una pregunta aparte y es la lección 7.

Y viste que la evidencia decisiva casi nunca está en el código. Está en el historial, en los datos de producción, en la frecuencia de cambio y en lo que hay bloqueado hoy. Un plano dice qué hay; solo la visita dice cómo se vive la casa.

Antes de avanzar deberías poder: enunciar la pregunta de la balanza; nombrar los dos rincones de Boletia y decir en qué dirección va cada uno con al menos dos datos; y explicar por qué "no tocar" es una decisión de ingeniería y no una omisión.

Lo que todavía no tienes es lo más importante para no hacer un desastre: el permiso para tocar. En la tabla de arriba hay una fila que no está y que la lección 2 va a agregar: ¿por qué está así?. Todo el código raro que vas a encontrar en tu carrera fue escrito por alguien que tenía una razón, y una parte de esas razones sigue siendo válida aunque no esté escrita en ningún lado. La lección 2 te da el procedimiento para averiguarlo antes de tocar —el historial, la culpa de cada línea, los incidentes, la persona— y te muestra una línea del checkout de Boletia que parece absurda, que tres personas quisieron borrar, y que tiene una razón excelente.

Recursos

  • Refactoring: Improving the Design of Existing Code (Martin Fowler) — la referencia del oficio. Lo que importa de este libro no es el catálogo de movimientos sino su definición de refactorizar: cambiar la estructura sin cambiar el comportamiento observable. Todo el módulo se apoya en esa frase.
  • Working Effectively with Legacy Code (Michael Feathers) — el libro sobre tocar código que da miedo, escrito por alguien que define "código heredado" como "código sin pruebas". Es el manual de fondo de las lecciones 2 y 5.
  • The Wrong Abstraction (Sandi Metz) — el ensayo corto que instala la idea de que una abstracción equivocada cuesta más que la duplicación que quería evitar. Es el argumento de la casilla de arriba a la derecha.
  • TechnicalDebt (Martin Fowler) — la metáfora de la deuda técnica, con su cuadrante de deuda prudente/imprudente y deliberada/inadvertida. Sirve para la lección 7: no toda deuda hay que pagarla, igual que no todo préstamo conviene liquidar antes de tiempo.