Módulo 8: Refactorizar con criterio (capstone)

4. Detectar el patrón que hay que quitar

Descripción

Al terminar esta lección vas a saber reconocer las tres formas de estructura que sobra —la interfaz con un solo implementador, la capa que solo reenvía llamadas y la configuración para algo que nunca varió—, vas a tener la prueba de una línea que las diagnostica a las tres, y vas a salir con la lista de lo que hay que revisar antes de quitar una sola línea. Esa lista es la mitad de la lección, porque quitar y agregar no son simétricos: agregar estructura es aditivo y se puede hacer con el sistema encendido; quitarla exige una certeza que agregar no necesita, la de que nadie más usa lo que vas a borrar.

Esto importa porque es el movimiento que casi ningún curso enseña, y por una razón que conviene decir en voz alta: quitar no se ve como trabajo. Un cambio que agrega una interfaz y tres clases parece ingeniería; uno que borra ciento cincuenta líneas parece limpieza. En una revisión, el primero se aprueba con un "se ve bien" y el segundo genera preguntas. Ese sesgo tiene consecuencias acumulativas: en un sistema de cinco años, la estructura solo entra y nunca sale, y el resultado es un código donde cada operación cuesta seis saltos y cuatro archivos. Aprender a quitar con criterio y a defenderlo es lo que evita ese destino.

Y hay un beneficio menos obvio. Quien sabe quitar abstracciones agrega mejores abstracciones, porque conoce el costo por dentro. La persona que ha desmontado un mecanismo de plugins innecesario piensa dos veces antes de crear el siguiente, y no por miedo: porque tiene medido lo que cuesta. La lección 3 y esta son las dos mitades del mismo criterio, no dos habilidades separadas.

Conexión con el módulo: la lección 3 te enseñó a reconocer que un código pide estructura, con la prueba del eje. Esta lección hace la lectura inversa de la misma prueba: cuando hay estructura para un eje que no existe, sobra. La lección 2 es la precondición obligatoria —no puedes quitar lo que no entendiste, y la cerca de Chesterton se aplica con especial dureza aquí, porque quitar es más difícil de revertir que agregar—. La lección 5 te da el procedimiento seguro para ejecutar el desmontaje, de afuera hacia adentro. La 6 te enseña a defenderlo, que en esta dirección importa el doble por el sesgo que acabamos de mencionar. Y la 7 te va a recordar que detectar estructura que sobra no obliga a quitarla: si nadie lee ese rincón y nada está bloqueado, la respuesta correcta puede ser dejarlo y anotarlo.

El andamio que quedó puesto

Pasas frente a un edificio terminado. Está habitado, tiene cortinas en las ventanas y macetas en los balcones. Y tiene el andamio puesto, en toda la fachada, desde hace tres años.

Nadie sube al andamio. Nadie lo usa. Pero está ahí, y produce tres efectos que se parecen mucho a los de una abstracción que sobra:

Tapa la vista. Para ver la fachada hay que mirar entre los tubos. Nadie puede decir de un vistazo si la pared tiene una grieta, porque hay un enrejado metálico delante.

Hay que mantenerlo. Se oxida, se le sueltan piezas, y una vez al año viene alguien a revisarlo. Ese mantenimiento no mejora el edificio: mantiene el andamio.

Y es un riesgo propio. En un temporal, lo que se cae no es el edificio: es el andamio. Un elemento que no aporta nada ha agregado una forma nueva de que algo salga mal.

Esa tercera es la que más se parece al software y la que más se olvida. El rincón plugins/ de Boletia tiró el checkout cuarenta minutos, y no por un error en la asignación de asientos: por un archivo a medias que el descubrimiento dinámico importó. La caída la causó el andamio, no el edificio. Cuando cuentes el costo de una abstracción que sobra, esa columna —incidentes atribuibles al mecanismo y no a la funcionalidad— suele ser la más contundente.

Ahora, la parte honesta de la analogía. Hay andamios que hay que dejar: si el edificio tiene una obra en marcha, o si la fachada se va a intervenir el mes que viene, quitarlo es un error. Por eso esta lección no dice "quita los andamios": dice averigua si hay obra. Y da la lista de dónde mirar.

Las tres formas de estructura que sobra

Forma 1 — La interfaz con un solo implementador

Qué es. Un contrato abstracto —clase base, Protocol, interfaz— con exactamente una implementación real.

Cómo se ve. Ya la conoces del módulo 2 con sus nueve señales. Las cinco más útiles para el diagnóstico rápido: una clase base con un implementador; un supports() o can_handle() que devuelve True sin condiciones; un registro o diccionario con una sola entrada; un directorio en plural —impls/, providers/, handlers/— con un solo archivo; y documentación de "cómo agregar un X" con cero X agregados.

Qué te está diciendo. Que la variación es imaginaria. Alguien predijo un eje que no ocurrió.

La evidencia que cierra el caso está en el historial, no en el código:

git log --diff-filter=A --name-only -- 'boletia/plugins/impls/*'

Cero implementaciones agregadas en dos años convierte una discusión de opiniones en un dato. Y ese dato no te enfrenta a quien escribió el código: te enfrenta al calendario.

Forma 2 — La capa que solo reenvía

Qué es. Un módulo, una clase o un conjunto de funciones cuyo cuerpo consiste en llamar a otra cosa con los mismos argumentos y devolver lo mismo. En la literatura se le llama middle man: un intermediario que no hace nada por el mensaje que transporta.

Cómo se ve. Así, y es sorprendentemente común:

# Archivo: services/order_service.py
from repositories import order_repository


class OrderService:
    """Capa de servicio para órdenes."""

    def get_order(self, order_id):
        return order_repository.get_order(order_id)

    def save_order(self, order):
        return order_repository.save_order(order)

    def list_orders_for_customer(self, customer_id):
        return order_repository.list_orders_for_customer(customer_id)

    def cancel_order(self, order_id):
        return order_repository.cancel_order(order_id)

Qué te está diciendo. Que hay una capa en el organigrama que no toma ninguna decisión. Y aquí está la prueba de una línea que diagnostica las tres formas de esta lección, la misma del módulo 2:

¿Qué deja de tener que saber quien llama, gracias a esta capa?

Aplícala a OrderService. ¿Qué deja de saber quien la usa? Nada: los nombres de los métodos son los mismos, los argumentos son los mismos, el resultado es el mismo. La capa redirige, no oculta. Compárala con una capa que sí se gana su lugar —payments/, por ejemplo, donde quien llama deja de saber que Stripe cobra en centavos enteros y que el efectivo no cobra sino que genera una referencia—. Esa diferencia, ocultar contra redirigir, es toda la lección en una frase.

Su excepción legítima, y hay que respetarla. Una capa que reenvía cuatro de cinco métodos y en el quinto hace una validación, un permiso o una transacción, no es un intermediario ocioso: es una capa con poca lógica, que es distinto. La pregunta correcta no es "¿cuántos métodos reenvían?" sino "¿alguna decisión del sistema se toma aquí?". Si la respuesta es sí, aunque sea en un método, la capa existe por una razón y lo que sobra son los reenvíos, no la capa.

Y otra excepción, esta más importante de lo que parece: una capa que hoy solo reenvía pero que se creó la semana pasada como primer paso de una migración está en obra. Ahí el andamio tiene sentido. Se pregunta, no se borra.

Forma 3 — La configuración para algo que nunca varió

Qué es. Un valor que se puede cambiar sin tocar código —una variable de entorno, una columna, una entrada en un archivo de configuración— y que nunca ha tenido más de un valor.

Cómo se ve. En Boletia hay dos ejemplares perfectos:

# Archivo: plugins/config.py
DEFAULT_PLUGIN = "default"


def get_configured_name(event) -> str:
    """Nombre del plugin configurado para este evento.

    `event.seating_plugin` es una columna de la tabla `events`.
    En los tres años que lleva existiendo, siempre ha valido "default".
    """
    return getattr(event, "seating_plugin", None) or DEFAULT_PLUGIN

Y su consecuencia visible, que es el detalle más elocuente de todo el rincón:

# Archivo: 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")]

Qué te está diciendo. Dos cosas. La primera: que la configurabilidad es teórica. La segunda, más grave: que el costo se filtró hasta la interfaz de usuario. Un campo de formulario con exactamente una opción no es una elección: es una pregunta que no existe, y alguien de operaciones la contesta cada vez que publica un evento.

La evidencia decisiva no está en el repositorio. Está en los datos:

-- Solo lectura. ¿Cuántos valores distintos ha tenido de verdad?
SELECT seating_plugin, count(*) FROM events GROUP BY 1;

Si el resultado es una sola fila con ciento ochenta mil eventos, la discusión terminó. Y fíjate en la asimetría con la que vas a trabajar el resto de la lección: quitar el código de configuración es reversible; borrar la columna no lo es. Van en tiempos distintos y con reglas distintas.

Su excepción legítima. Una configuración que existe para poder apagar algo en una emergencia —un interruptor de pánico— tiene un solo valor observado justamente porque nunca hubo emergencia. No se toca. La pregunta de control: ¿este valor existe para variar, o para poder reaccionar? Si es lo segundo, su falta de uso es una buena noticia, no una señal.

La prueba única, y su versión invertida

Las tres formas se diagnostican con la misma pregunta y conviene tenerla en las dos direcciones:

Directa: ¿qué deja de tener que saber quien usa esto, gracias a esta capa? Invertida: si borro esta capa y conecto los dos extremos, ¿qué información tendría que saber quien llama, que hoy no sabe?

La invertida es la más útil en la práctica, porque se puede contestar mentalmente en treinta segundos y no requiere leer la implementación entera. Hazla con OrderService: si la borras, quien llamaba a service.get_order(id) llamaría a order_repository.get_order(id). No tendría que saber nada nuevo. Sobra.

Hazla ahora con el plugins/ de Boletia: si la borras, el checkout llamaría a assign_seat(ticket) en vez de a plugin.assign_seat(order, ticket). ¿Qué tendría que saber de más? Nada —de hecho tendría que saber menos, porque el parámetro order no se usaba y existía solo porque la interfaz lo pedía—. Sobra, y con ciento ochenta y tres líneas encima.

Y hazla con payments/ después del módulo 4: si la borras, quien cobra tendría que saber que Stripe quiere centavos enteros, que MercadoPago quiere un float y una descripción, y que el efectivo no cobra. Tres conocimientos incompatibles. No sobra.

Ejemplo trabajado: diagnosticar una capa que reenvía

Vamos a hacer el diagnóstico completo de services/order_service.py, porque es el caso menos obvio de los tres y el más frecuente en sistemas reales.

Paso 1 — Mide la proporción entre reenvío y trabajo.

Abre el archivo entero y clasifica cada método en dos columnas: los que reenvían y los que hacen algo.

# Archivo: services/order_service.py    (los 6 métodos, completos)

class OrderService:

    def get_order(self, order_id):
        return order_repository.get_order(order_id)                    # reenvía

    def save_order(self, order):
        return order_repository.save_order(order)                      # reenvía

    def list_orders_for_customer(self, customer_id):
        return order_repository.list_orders_for_customer(customer_id)  # reenvía

    def cancel_order(self, order_id):
        return order_repository.cancel_order(order_id)                 # reenvía

    def orders_for_event(self, event_id):
        return order_repository.orders_for_event(event_id)             # reenvía

    def total_sold_for_event(self, event_id):
        # ← ¡aquí sí pasa algo!
        orders = order_repository.orders_for_event(event_id)
        return round(sum(o.total for o in orders if o.status == "paid"), 2)

Cinco de seis reenvían. Uno hace un cálculo. Ese uno cambia el diagnóstico y por eso el paso 1 es contar y no juzgar de un vistazo.

Paso 2 — Aplica la prueba invertida a cada método, no a la clase.

Este es el matiz que convierte el diagnóstico en útil. La pregunta no es "¿sobra OrderService?" sino "¿sobra cada uno de sus métodos?".

MétodoSi lo borro, ¿qué tendría que saber quien llama?Veredicto
get_ordernadasobra
save_ordernadasobra
list_orders_for_customernadasobra
cancel_ordernadasobra
orders_for_eventnadasobra
total_sold_for_eventque hay que filtrar por status == "paid" y redondearno sobra

total_sold_for_event es conocimiento de negocio real: define qué cuenta como "vendido". Si desapareciera, ese filtro se escribiría a mano en cada lugar que necesite el total, y tarde o temprano alguien se olvidaría del status == "paid" y reportaría de más.

Paso 3 — Busca quién usa qué, por módulo.

grep -rn "order_service\|OrderService" --include='*.py' .
boletia/api/routes.py:14:      from services.order_service import OrderService
boletia/api/routes.py:88:         order = OrderService().get_order(order_id)
boletia/admin/panel.py:31:       from services.order_service import OrderService
boletia/admin/panel.py:52:        total = OrderService().total_sold_for_event(event.id)
boletia/reports/sales.py:9:      from services.order_service import OrderService
boletia/reports/sales.py:22:      rows = OrderService().orders_for_event(event_id)
tests/test_order_service.py:1:   from services.order_service import OrderService

Búsqueda por módulo, no por método. Es la lección del usuario número cuatro del módulo 2: cuando alguien importa una variable privada o instancia la clase por su nombre, una búsqueda por el método no lo encuentra.

Paso 4 — Revisa las excepciones legítimas antes de decidir.

Aquí está la lista de control, y hay que pasarla entera:

  • ¿Hay una segunda implementación en algún lado? Búscala en tests/, en otros entornos y en otros repositorios. → No la hay: OrderService no es una interfaz, es una clase concreta.
  • ¿Es una frontera pública? ¿Alguien fuera de este repositorio la consume? → No: Boletia es un servicio único, mantenido por un equipo de seis personas.
  • ¿La exige el entorno? ¿Algún framework requiere esta forma? → No.
  • ¿Está en obra? ¿Se creó como primer paso de una migración en curso? → El historial dice que tiene tres años y nueve commits, todos de agregar métodos de reenvío. No hay obra.
  • ¿Es un interruptor de emergencia? No aplica a esta forma.

Cinco preguntas, cinco noes. Solo ahora tienes permiso para decidir.

Paso 5 — Decide, y date cuenta de que la decisión no es binaria.

Lo que se hace no es "borrar OrderService". Es esto:

  1. Los cinco reenvíos se quitan: quien los llamaba pasa a llamar al repositorio directamente.
  2. total_sold_for_event se queda, pero no como método de una clase sin estado. Se convierte en una función de módulo, en el lugar donde vive el conocimiento que encapsula.
  3. La clase desaparece, porque una clase sin estado con un solo método es un espacio de nombres disfrazado, y en Python un módulo ya es un espacio de nombres.
# Archivo: reports/sales.py    (el resultado)

def total_sold_for_event(event_id) -> float:
    """Total vendido de un evento. Solo cuentan las órdenes pagadas."""
    orders = order_repository.orders_for_event(event_id)
    return round(sum(o.total for o in orders if o.status == "paid"), 2)

Qué esperar de este diagnóstico. Cuatro observaciones.

Primera: el resultado no fue "sobra" ni "no sobra". Fue "sobran cinco de seis, y el sexto cambia de casa". Ese tipo de resultado es el normal cuando el diagnóstico se hace por método y no por archivo, y es mucho mejor que cualquiera de los dos extremos. Quien decide por archivo o borra conocimiento de negocio o conserva cinco reenvíos inútiles.

Segunda: el método que se salvó es el que contenía una decisión. Ese es el criterio, y sirve para toda la lección: una capa se gana su lugar por las decisiones que toma, no por los métodos que expone. Cinco métodos que reenvían valen menos que uno que sabe qué cuenta como vendido.

Tercera: no se reemplazó por nada. No apareció un OrderFacade, ni un OrderManager "más simple". Ese impulso —dejar algo en el lugar de lo que quitaste— es el error más común de esta dirección y tiene un nombre en el módulo 2: sobre-corrección. La respuesta correcta a "una capa que no hace nada" no es "una capa más pequeña que no hace nada".

Cuarta: el trabajo real fue el paso 4. Los pasos 1 a 3 tomaron diez minutos; la lista de control es la que da el permiso, y es la que separa un desmontaje profesional de un borrado optimista. Fíjate en que las cinco preguntas se contestan con búsquedas y con el historial, no con opinión.

Qué revisar antes de quitar: la lista completa

Junta lo que viste en esta lección y en el módulo 2 y queda una lista de siete verificaciones. Hazlas todas antes de tocar una línea; en conjunto no toman más de media hora y son la diferencia entre un desmontaje y un incidente.

1. Cuenta las implementaciones en todo el repositorio, incluidos los tests. La pregunta correcta no es "¿cuántas clases heredan de esto?" sino "¿cuántos comportamientos distintos se ejecutan de verdad, en algún entorno?". Un doble de prueba que se usa en ochenta tests es una segunda implementación real.

2. Busca por módulo, no por símbolo. grep -rn "plugins\." boletia/ encuentra los cinco usuarios de plugins/; grep -rn "get_for" encuentra tres y deja fuera al panel, que importa _REGISTRY directamente. El guion bajo de una variable privada significa "no deberías usar esto desde fuera", no "nadie lo usa desde fuera".

3. Mira el historial: ¿cuántas veces se ejercitó el mecanismo? Implementaciones agregadas, valores distintos de la configuración, commits que no sean de mantenimiento. Este es el dato que convierte tu propuesta en un argumento.

4. Mira los datos de producción, si la abstracción llega hasta ahí. Una columna, un campo de configuración, un valor guardado. Y recuerda la regla: el código es reversible; los datos no. Quitar el código que escribe una columna va hoy; borrar la columna va en otra ventana, con respaldo, después de confirmar que nadie la lee.

5. Pregunta si hay una obra en marcha. Una capa recién creada puede ser el primer paso de una migración. Una interfaz con un implementador puede estar esperando al segundo, que ya está firmado. Esta pregunta se le hace a una persona, no al código, y es la única de la lista que no se puede automatizar.

6. Revisa las cinco excepciones legítimas del módulo 2. Frontera de sistema externo con doble de prueba; frontera pública que otros repositorios consumen; exigencia del entorno o del framework; segunda implementación que vive en otro repositorio; variación de entorno que ya está en uso —un reloj congelado en pruebas, un almacenamiento local en desarrollo—.

7. Mira las pruebas que vas a heredar. Si hay pruebas del andamiaje —que verifican que el registro registra, que el descubrimiento descubre—, van a morir con el mecanismo, y eso está bien: prueban algo que va a dejar de existir. Pero antes de borrarlas, comprueba si alguna prueba algo que solo ellas prueban. En Boletia, los cuatro tests de plugins/ no cubren ni una vez que un asiento se asigne bien; el desmontaje, además de quitar líneas, agrega la primera prueba real del comportamiento.

Y una regla que resume el espíritu de la lista: borra únicamente lo que no aparece en ningún grep. La búsqueda es tu permiso para borrar; no la memoria, no la intuición, no el hecho de que "esto claramente no lo usa nadie".

Cómo se desmonta sin miedo

El procedimiento lo viste completo en el módulo 2 y lo vas a ejecutar en la lección 5. Aquí van las dos reglas que lo gobiernan, porque son las que hacen que "sin miedo" sea literal y no una frase motivadora.

Regla 1 — De afuera hacia adentro. Primero cortas el uso, después borras lo que quedó sin usar. Al revés no funciona: si empiezas borrando la interfaz, se rompen a la vez la implementación, el registro, los tres puntos de llamada y las pruebas, y pierdes la capacidad de avanzar en pasos verificables. La interfaz se siente como el problema, así que es lo primero que uno quiere quitar; es la base de la pirámide, no la punta.

Regla 2 — Cada paso deja el sistema funcionando. Y de ahí sale la propiedad que quita el miedo de verdad: puedes detenerte después de cualquier paso y el resultado ya es mejor que el original. Un desmontaje que solo sirve si lo terminas completo es un desmontaje mal diseñado, porque el trabajo real siempre se interrumpe —una urgencia, una reunión, un viernes—.

Piensa en lo que esas dos reglas hacen con el riesgo. Sin ellas, quitar una abstracción es una apuesta: o sale entera o revierte entera. Con ellas, es una secuencia de cambios chicos, cada uno revisable en un minuto y revertible por separado. El miedo a tocar un rincón no viene del rincón: viene de no tener un procedimiento que permita parar.

Errores comunes

Diagnosticar por archivo en vez de por método (de diagnóstico). Qué pasa: alguien abre OrderService, ve que casi todo reenvía, y borra la clase entera. Con ella se va total_sold_for_event, y el filtro status == "paid" se reescribe a mano en tres lugares. Dos meses después, el panel del organizador reporta ventas de más porque alguien copió la suma sin el filtro. Por qué pasa: el veredicto se siente binario, y el archivo es la unidad en la que uno lee. Además, cinco de seis métodos apuntando en la misma dirección crean una inercia difícil de frenar. Cómo detectarlo: si tu diagnóstico es una sola palabra para un archivo con seis métodos, no diagnosticaste, generalizaste. Cómo corregirlo: la tabla del paso 2, una fila por método, con la prueba invertida contestada en cada una. Y una regla: antes de borrar cualquier cosa, pregúntate qué conocimiento del negocio vive ahí dentro. Si vive alguno, no se borra: se muda.

Sobre-corregir: reemplazar la abstracción por una más chica (de método). Qué pasa: alguien quita el registro de plugins y, como entregar un cambio que solo borra se siente incorrecto, deja en su lugar una clase SeatingService con tres métodos. O una interfaz "más simple, de dos métodos en vez de cinco". Todas esas son la misma abstracción con menos calorías: siguen teniendo una sola implementación, siguen siendo un concepto privado del proyecto que hay que explicarle a quien entra, y siguen agregando un salto. Por qué pasa: por el sesgo del que hablamos al principio —quitar no se ve como trabajo— y porque una clase se siente más "profesional" que tres funciones sueltas. Cómo detectarlo: si al terminar tu desmontaje sigue existiendo un concepto nuevo del proyecto para hacer lo mismo, sobre-corregiste. Cómo corregirlo: la respuesta correcta a "una abstracción con un solo implementador" es una función, no otra abstracción. Y si la clase que queda no tiene estado —ni __init__, ni atributos, ni self usado en los métodos—, es un espacio de nombres disfrazado, y en Python un módulo ya cumple esa función.

Confundir "no lo entiendo" con "sobra" (de riesgo). Qué pasa: alguien llega nuevo, encuentra un rincón que le cuesta seguir, y lo marca como sobre-diseñado. Propone quitarlo. En la revisión aparece que esa capa aislaba una API externa que devuelve tres formatos distintos según la hora del día, y que lo que parecía indirección gratuita era lo único que impedía que ese desorden se filtrara al resto del sistema. Por qué pasa: la dificultad de lectura y la estructura innecesaria producen la misma sensación —"aquí hay demasiadas vueltas"— y desde fuera no se distinguen. Cómo detectarlo: si tu argumento para quitar algo es que te costó entenderlo, y no que la prueba invertida dio "nada", no tienes un diagnóstico. Cómo corregirlo: contesta la prueba invertida por escrito, con la lista de lo que quien llama tendría que saber. Si la lista está vacía, sobra. Si tiene tres cosas incompatibles entre sí, no sobra y lo que te costó fue leer, no la estructura. Y recuerda la lección 2: la complejidad que no entiendes puede ser la cicatriz de un problema real.

Ejercicios

Ejercicio 1 — Aplica la prueba invertida. Para cada capa, contesta "si la borro y conecto los dos extremos, ¿qué tendría que saber quien llama, que hoy no sabe?" y decide si sobra. Dos líneas por caso.

(a) notifications/channel.py: la interfaz NotificationChannel con send(destination, message), implementada por correo (SMTP), SMS (una API externa que corta a 160 caracteres) y push (otra API, con token). (b) db/query_helper.py: def run(sql, *params): return connection.execute(sql, params), usado en cuarenta lugares. (c) payments/provider.py: la interfaz PaymentProvider, con Stripe, MercadoPago y efectivo detrás. (d) utils/config_wrapper.py: def get(key): return os.environ.get(key), usado en doce lugares. (e) storage/file_store.py: interfaz con una implementación en producción (un bucket) y otra en desarrollo (una carpeta local), las dos en uso.

Ver solución

(a) No sobra. Quien llama tendría que saber que el correo se manda por SMTP con un asunto, que el SMS corta a 160 caracteres y se cobra por segmento, y que el push necesita un token que puede ser None. Tres conocimientos incompatibles ocultados: la capa oculta, no redirige.

(b) Sobra casi seguro, pero mira antes si hay una decisión escondida. Si el cuerpo es literalmente ese, no oculta nada: quien llama pasa el mismo SQL y los mismos parámetros. Ahora bien, si dentro hubiera un registro de consultas lentas, un tiempo límite o el manejo de la transacción, entonces sí toma una decisión y se queda. Es exactamente el caso del sexto método de OrderService: el diagnóstico depende del cuerpo, no del nombre.

(c) No sobra, y es el contraste que hace útil el ejercicio. Quien llama tendría que saber que Stripe quiere centavos enteros, que MercadoPago quiere un float y una descripción, y que el efectivo no cobra sino que genera una referencia y termina en "pendiente". Tres formas incompatibles de decir "cóbrame".

(d) Sobra. get("X") contra os.environ.get("X"): quien llama no deja de saber absolutamente nada, y además se agregó un concepto propio del proyecto para no escribir una llamada estándar. Doce puntos de llamada no lo justifican; al contrario, son doce lugares donde alguien tiene que aprender un nombre nuevo. La excepción sería que la envoltura hiciera algo real —convertir tipos, fallar si falta la clave, leer de un almacén de secretos—, y entonces el nombre debería decirlo.

(e) No sobra, y es la quinta excepción legítima del módulo 2: variación de entorno en uso. Hay dos implementaciones y las dos se ejecutan, cada una en su entorno. Nota que aquí la interfaz tiene un solo implementador en producción, y aun así el olor no aplica. Contar implementaciones sin contar entornos es el error clásico.

Por qué funciona: de los cinco, dos sobran y tres no, y los cinco se ven estructuralmente parecidos —una interfaz o una envoltura delante de algo—. Lo que decide no es la forma: es qué información queda encapsulada del otro lado.

Ejercicio 2 — Diagnostica por método. Aquí está una clase de Boletia. Haz la tabla del paso 2 —una fila por método, la prueba invertida contestada— y di qué harías con cada uno.

# Archivo: services/event_service.py

class EventService:

    def get_event(self, event_id):
        return event_repository.get_event(event_id)

    def publish(self, event):
        if event.starts_at <= utcnow():
            raise CannotPublishPastEvent(event.id)
        if not event.venue:
            raise MissingVenue(event.id)
        event.status = "published"
        return event_repository.save_event(event)

    def list_events(self):
        return event_repository.list_events()

    def seat_map(self, event):
        plugin = get_seating_plugin(event)
        return plugin.render_map(event)
Ver solución
Método¿Qué tendría que saber quien llama?Veredicto
get_eventnadasobra; quien lo usa llama al repositorio
publishlas dos reglas de publicación y el estado correctose queda: aquí vive el negocio
list_eventsnadasobra
seat_mapcómo se obtiene el plugin y que hay que llamar a render_mapcaso especial, ver abajo

publish es el corazón real de esta clase. Dos reglas de negocio —no se publica un evento que ya pasó, no se publica sin sede— y una transición de estado. Ese método justifica que exista un lugar llamado "eventos" con lógica propia; lo que no justifica es que ese lugar sea una clase sin estado. Se convierte en una función de módulo, events/publishing.py :: publish(event).

seat_map es el interesante, y es una trampa deliberada. Hoy oculta algo: quien llama no tiene que saber que existe un registro de plugins. Así que en el estado actual no sobra. Pero fíjate en lo que eso significa: es una capa que solo existe para tapar otra capa que sobra. Cuando desmontes plugins/, este método pasará a ser return render_map(event), un reenvío puro, y entonces sí sobrará.

Esa es una dinámica que vale la pena reconocer, porque aparece siempre: quitar una abstracción innecesaria vuelve innecesarias a las que la envolvían. Por eso el desmontaje se hace de afuera hacia adentro y se re-evalúa al final, en vez de decidirlo todo el primer día.

Por qué funciona: la clase tiene cuatro métodos y cuatro veredictos distintos —uno sobra, uno se queda, uno sobra, uno "depende de otro trabajo"—. Ese resultado desordenado es el realista, y es imposible de obtener si diagnosticas por archivo.

Ejercicio 3 — La lista de control, contestada. Vas a proponer el desmontaje de plugins/ en Boletia. Contesta las siete verificaciones de la lección con los datos que ya conoces, y di cuál de las siete es la que más riesgo tiene de salir mal si la haces por encima.

Ver solución
  1. Implementaciones en todo el repositorio, incluidos los tests: una, DefaultSeatingPlugin. En tests/ no hay una segunda real; el cuarto test registra a mano un plugin falso para poder llegar al raise SeatingPluginNotFound, lo cual no es una implementación en uso sino un truco para alcanzar una rama inalcanzable en producción.
  2. Búsqueda por módulo: grep -rn "plugins\." boletia/ devuelve cinco usuarios —checkout, el endpoint del mapa de asientos, una tarea programada, el panel de administración y los tests del andamiaje—. Buscar por get_for habría devuelto tres y habría dejado fuera al panel, que importa _REGISTRY directamente.
  3. Historial: cero implementaciones agregadas en dos años; tres commits, todos de mantenimiento —un renombre de carpeta, un arreglo de pkgutil tras subir a Python 3.11, y un log.debug—; y un incidente de cuarenta minutos con el checkout caído por un archivo a medias que el descubrimiento importó.
  4. Datos de producción: events.seating_plugin vale "default" en todos los eventos publicados. La columna no se borra en este trabajo: se deja de escribir, se confirma que nadie la lee, y se elimina después en una ventana aparte con respaldo.
  5. ¿Hay obra en marcha? Hay que preguntarlo. Si existiera un contrato firmado con un recinto que va a conectar su propio sistema de butacas, la respuesta cambia por completo y el mecanismo se conserva —y se arregla, porque fue diseñado sin ningún consumidor real—.
  6. Excepciones legítimas: ninguna aplica. No hay entrada y salida real detrás de la interfaz que justifique un doble de prueba —la implementación habla con la base igual—, no es una frontera pública, no la exige ningún framework, no hay segunda implementación en otro repositorio, y no es variación de entorno.
  7. Pruebas que se heredan: cuatro tests, sesenta y siete líneas, que prueban el registro y el descubrimiento. Ninguno prueba que un asiento se asigne bien. Mueren con el mecanismo, y el desmontaje agrega las primeras pruebas de comportamiento que ha tenido ese rincón.

La que más riesgo tiene de salir mal por encima: la número 2, la búsqueda por módulo. Y no por dificultad técnica, sino por lo que se busca. Si buscas por el nombre de la función pública encuentras tres de cinco usuarios; el panel se rompe silenciosamente y el fallo aparece días después, cuando alguien de operaciones intenta publicar un evento. Es el error clásico, y su antídoto es una sola letra de diferencia en el comando.

La número 5 es la segunda más peligrosa, por otra razón: es la única que no se contesta con una herramienta. Un grep mal escrito lo puedes repetir; una conversación que no tuviste no aparece en ningún reporte.

Por qué funciona: acabas de hacer la mitad del trabajo previo del proyecto final, y de paso confirmaste algo que conviene decir explícitamente: seis de las siete verificaciones dieron "adelante", una quedó pendiente de una respuesta humana, y aun así todavía falta la pregunta de la lección 7 —si hoy es el día de hacerlo—.

Resumen y siguiente paso

En esta lección aprendiste el movimiento que casi nadie enseña: reconocer la estructura que sobra. Las tres formas son la interfaz con un solo implementador, la capa que solo reenvía —el intermediario que no hace nada por el mensaje que transporta— y la configuración para algo que nunca varió, que en Boletia llegó a filtrarse hasta un desplegable de una sola opción en el formulario de eventos.

Las tres se diagnostican con la misma pregunta, y su versión invertida es la que se contesta en treinta segundos: si borro esta capa y conecto los dos extremos, ¿qué tendría que saber quien llama, que hoy no sabe? Si la respuesta es "nada", la capa redirige en vez de ocultar. Si son tres cosas incompatibles entre sí —centavos enteros, un float con descripción, una referencia que no cobra—, la capa se gana su lugar.

Viste que el diagnóstico se hace por método, no por archivo, y por qué: en OrderService, cinco de seis métodos sobraban y el sexto contenía la definición de qué cuenta como una venta. Borrar por archivo habría sacado ese conocimiento del código y lo habría dispersado a mano por tres lugares. La regla que queda: una capa se gana su lugar por las decisiones que toma, no por los métodos que expone.

Y te llevas la lista de siete verificaciones previas: contar implementaciones en todo el repositorio incluidos los tests; buscar por módulo y no por símbolo; mirar el historial; mirar los datos de producción recordando que el código es reversible y los datos no; preguntarle a una persona si hay una obra en marcha, que es la única verificación que ninguna herramienta hace; revisar las cinco excepciones legítimas; y mirar qué pruebas vas a heredar. Más la regla que las resume: borra únicamente lo que no aparece en ningún grep.

Antes de avanzar deberías poder: nombrar las tres formas con un ejemplo de cada una; aplicar la prueba invertida a una capa cualquiera; explicar por qué el diagnóstico va por método; y enunciar al menos cinco de las siete verificaciones previas.

Ya tienes los dos diagnósticos, en las dos direcciones. Lo que falta es la parte de las manos: cómo se ejecuta cualquiera de los dos sin romper nada. La lección 5 se ocupa de eso y es la más práctica del módulo. Vas a ver qué es exactamente un "paso seguro" —su precondición, su transformación, su verificación y su punto de parada—, por qué la propiedad de poder detenerte a la mitad es lo que quita el miedo, cómo se construye la red de seguridad que lo hace posible, y por qué el refactor gigante de una sentada, ese que se hace un viernes con la mejor intención, casi siempre termina en un revert el lunes.

Recursos

  • Speculative Generality (Refactoring Guru) — el nombre formal de la primera forma, con sus refactorizaciones asociadas: Collapse Hierarchy, Inline Class, Remove Parameter. Es el catálogo del desmontaje.
  • Middle Man (Refactoring Guru) — el nombre formal de la segunda forma, la capa que solo reenvía, con su refactorización Remove Middle Man. Incluye la advertencia sobre cuándo el intermediario sí se justifica.
  • Refactoring: Improving the Design of Existing Code (Martin Fowler) — los movimientos mecánicos que ejecutaste, cada uno con su procedimiento y sus condiciones de seguridad: Inline Function, Inline Class, Collapse Hierarchy, Replace Superclass with Delegate.
  • Parallel Change / Expand and Contract (Martin Fowler) — el patrón formal para cambiar algo de lo que otros dependen sin romperlos, en tres fases. Es la versión rigurosa de lo que hay que hacer con la columna de la base de datos, y sirve cada vez que toques un esquema o una interfaz que alguien más consume.