Módulo 2: Cuándo NO abstraer

8. Proyecto: quita una abstracción que sobra

Descripción

Al terminar este proyecto vas a haber desmontado una abstracción real, con sus complicaciones reales, sin romper el comportamiento del sistema. Y vas a haber escrito el documento que acompaña ese trabajo, que es la mitad del entregable y —seamos honestos— la mitad que de verdad se evalúa en un equipo: qué costaba esa abstracción, qué se gana al quitarla, y bajo qué condición concreta habría que volver a ponerla.

Este proyecto no se juzga por cuántas líneas borres. Se juzga por dos cosas: que el comportamiento observable no haya cambiado ni un carácter, y que tu razonamiento sea defendible frente a alguien que no está de acuerdo. Puedes hacer un desmontaje impecable y entregarlo mal; puedes tener razón y no poder demostrarlo. Las dos cosas se practican aquí.

Y hay una diferencia importante con lo que viste en la lección 6. Allí te mostré el procedimiento sobre una versión simplificada del rincón: un solo sitio de llamada, el del checkout. El código real de Boletia tiene cinco usuarios del mecanismo, y uno de ellos hace algo que va a romper tu desmontaje si no lo encuentras a tiempo. El paso 2 del procedimiento —buscar quién más usa esto— existe precisamente por eso, y en este proyecto vas a descubrir por qué.

Conexión con el módulo: este proyecto usa las siete lecciones. La 2 te da el inventario del costo con el que abre tu documento. La 3 te da el argumento de por qué no había información para abstraer. La 4 te da el procedimiento de salida y la advertencia de no re-abstraer por reflejo. La 5 te da la distinción entre lo reversible y lo que no —que es la que decide qué hacer con la columna de la base de datos—. La 6 te da los seis pasos y el olor con sus nueve señales. Y la 7 te da los tres ejes con los que vas a redactar la condición de reversión, que es la sección que separa una opinión de un criterio.

Un martes cualquiera

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

"Necesitamos agregar un modo de asignación de asientos para grupos —que las butacas de una misma orden queden juntas— y nadie quiere meterse en plugins/. La última vez que alguien tocó esa carpeta se cayó el checkout cuarenta minutos. ¿Puedes dejarlo entendible antes de que agreguemos nada?"

Fíjate en la forma del encargo, porque es la forma real. Nadie te pidió "quita la sobre-ingeniería". Te pidieron poder hacer un trabajo que está bloqueado por ella. 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 solo una preferencia estética.

Un segundo detalle: te pidieron dejarlo entendible antes de agregar el modo de grupos, no como parte del mismo cambio. Eso es correcto y conviene defenderlo. 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ó. Si algo sale mal, tampoco sabes cuál de las dos cosas lo causó.

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

  1. Dejar el rincón entendible, sin cambiar el comportamiento. ← este proyecto.
  2. Agregar el modo de grupos, ahora sobre código directo. ← no es este proyecto, pero tu documento tiene que dejarlo preparado.

Ese segundo tiempo importa para tu entrega, porque es justamente el escenario de la condición de reversión. Cuando existan dos o tres algoritmos de asignación reales, ¿hay que volver a poner un mecanismo de extensión? Tu documento tiene que contestar eso, y la respuesta —lo viste en la lección 7— no es "sí" ni "no": es "un peldaño, no cuatro".

Ejemplo trabajado: un desmontaje completo, en pequeño

Antes de meterte con el rincón grande, vamos a hacer uno chiquito de principio a fin, para que veas el método completo —incluido el documento— en algo que cabe en una página.

El caso es el MoneyFormatter de la lección 2: una interfaz, una implementación, una fábrica, para formatear pesos mexicanos.

Paso 0 — La red. Un test de comportamiento que no menciona la estructura:

# Archivo: tests/test_money_format.py
def test_formats_with_thousands_separator_and_two_decimals():
    assert format_money_for(1250.0) == "$ 1,250.00 MXN"

def test_formats_small_amounts():
    assert format_money_for(9.5) == "$ 9.50 MXN"

def test_formats_zero():
    assert format_money_for(0) == "$ 0.00 MXN"

format_money_for es un envoltorio de una línea que hoy llama a la fábrica y mañana llamará a la función directa. Ese envoltorio es lo que permite que el mismo test valga antes y después.

Paso 1 — Cortar el uso. En cada sitio de llamada:

- from formatting.factory import get_formatter
+ from utils.money import format_money

- total = get_formatter("MXN").format(order.total)
+ total = format_money(order.total)

Paso 2 — Buscar quién más. grep -rn "get_formatter\|MoneyFormatter" boletia/ → dos sitios más, en el exportador de PDF y en el panel. Se repite el paso 1 en los dos.

Paso 3 — Borrar el andamiaje. Fuera formatting/factory.py.

Paso 4 — Colapsar la jerarquía. MXNFormatter ya no hereda de nada; fuera formatting/money.py.

Paso 5 — Convertir en función. La clase no tenía estado; su método format se convierte en utils/money.py :: format_money().

Paso 6 — Limpiar. No hay columnas ni configuración; solo imports muertos.

Resultado: tres archivos y cuarenta y un líneas se convierten en uno y cinco. Los tres tests pasan sin cambiar una letra. Tiempo total: unos veinticinco minutos.

Y ahora el documento, que es más largo que el diff y así debe ser:

Contexto. Boletia cobra únicamente en pesos mexicanos y se despliega como un solo servicio, mantenido por un equipo de seis personas. Nadie fuera de este repositorio depende de este código.

Qué costaba. Tres archivos y 41 líneas para producir 5. Tres saltos para responder "¿cómo se ve un monto?" cuando la respuesta es una línea de formato. Tres conceptos privados del proyecto (MoneyFormatter, MXNFormatter, get_formatter) que hay que explicarle a quien entra. Una implementación desde que existe. La capa no ocultaba nada: quien la usaba tenía que saber lo mismo que ahora, y además tenía que saber pedir un formateador y con qué código.

Qué se gana. Un salto en vez de tres. Cero conceptos nuevos. El código de moneda deja de estar escrito a mano en los sitios de llamada, que era lo que la capa decía centralizar y no centralizaba. Comportamiento idéntico, verificado con tres tests escritos antes de tocar nada.

Cuándo la volvería a poner. Cuando Boletia venda en una segunda moneda con un formato distinto de verdad —separador de miles y decimal invertidos, o símbolo pospuesto—. En ese momento bastaría un diccionario de formatos por código de moneda: una interfaz solo se justificaría si cada moneda necesitara lógica propia y no solo parámetros de formato. Si Boletia se publicara como paquete que otros equipos consumen, la respuesta cambiaría antes: ahí el punto de extensión sí valdría, porque cambiar la firma implicaría coordinar con gente que no controlamos.

Qué esperar de este ejemplo. Tres cosas.

Primera: el documento es más largo que el cambio, y está bien. El diff son treinta y seis líneas menos. El documento son cuatro párrafos. Esa proporción es correcta y va a ser peor —o mejor, según cómo lo veas— en el proyecto real. Lo que se evalúa es el criterio, y el criterio vive en el documento.

Segunda: mira cómo está escrita cada sección. "Qué costaba" son números: tres archivos, cuarenta y un líneas, tres saltos, tres conceptos, una implementación. Ni un adjetivo. "Cuándo la volvería a poner" es verificable: alguien puede abrir el roadmap y ver si hay una segunda moneda. Y tiene dos niveles, porque distingue el caso probable —un diccionario alcanza— del caso que justificaría la abstracción completa.

Tercera: el documento reconoce un contexto en el que la decisión contraria sería correcta. Esa última frase sobre el paquete público no es humildad decorativa: es lo que demuestra que entendiste el tradeoff y no solo aplicaste una regla. Quien lee tu propuesta y ve que ya consideraste el caso opuesto confía mucho más en tu conclusión.

El código de partida

Ahora el rincón real. Los cinco archivos de plugins/ son los que viste completos en la lección 6 —base.py, registry.py, config.py, impls/default_seating.py y el __init__.py—. Vuelve a ellos si necesitas el detalle; aquí está lo que la lección 6 no te mostró: quién más lo usa.

Usuario 1 — el checkout (el que ya conoces):

# Archivo: checkout/checkout.py   (fragmento)
from plugins.registry import get_for as get_seating_plugin

def checkout(order):
    ...
    event = load_event(order)
    plugin = get_seating_plugin(event)
    for ticket in tickets:
        ticket.seat = plugin.assign_seat(order, ticket)
    ...

Usuario 2 — el endpoint del mapa de asientos:

# Archivo: api/routes.py   (fragmento)
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 — una tarea programada:

# Archivo: admin/commands.py   (fragmento)
from plugins.registry import get_for as get_seating_plugin

def release_expired_reservations():
    """Libera los asientos de reservas vencidas. Corre cada 10 minutos."""
    for ticket in find_expired_reserved_tickets():
        event = load_event(ticket.event_id)
        get_seating_plugin(event).release_seat(ticket)
        ticket.status = "available"
        save(ticket)

Usuario 4 — el panel de administración:

# Archivo: admin/panel.py   (fragmento)
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 — los tests del andamiaje:

# Archivo: 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, porque son los dos puntos donde este proyecto deja de ser copiar la lección 6.

El panel de administración importa _REGISTRY, que es una variable privada del módulo, para llenar un desplegable. Si borras el registro, el formulario de eventos deja de funcionar. Y aquí está la parte interesante: ese desplegable tiene exactamente una opción. No es una elección; es un campo de formulario que finge ser una elección. Qué hacer con él es una decisión de diseño que tu documento tiene que justificar.

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

El encargo, paso a paso

Paso 0 — Levanta el inventario y la evidencia.

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

# Cuántos archivos y líneas
find boletia/plugins -name '*.py' | xargs wc -l

# Quién usa el mecanismo, de verdad
grep -rn "plugins\." boletia/ --include='*.py'

# Cuántas implementaciones se agregaron desde que existe
git log --diff-filter=A --name-only -- 'boletia/plugins/impls/*'

# Qué se ha tocado ahí en los últimos dos años
git log --since="2 years ago" --oneline -- boletia/plugins/

Y si tuvieras acceso a producción —en este proyecto usa los datos que te dio la guía—: cuántos valores distintos tiene events.seating_plugin y cuántas veces se devolvió el 404 del mapa de asientos.

Paso 1 — Escribe el test de caracterización.

Cuatro o cinco tests sobre el comportamiento observable, sin mencionar plugins ni registro. Tienen que cubrir los tres verbos que el mecanismo expone —asignar, liberar y pintar el mapa— porque hay tres usuarios distintos y cada uno usa uno.

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 2 — Corta el uso, un usuario por commit.

Los cuatro usuarios de producción, en cuatro cambios separados. Empieza por el que menos riesgo tiene —probablemente la tarea programada— y deja el checkout para cuando ya hayas hecho dos y tengas confianza en el método.

El usuario 4, el panel, no se resuelve igual que los otros tres: no llama a get_for, lee el registro. Ahí tienes que decidir qué hacer, y la decisión va en el documento.

Paso 3 — Verifica que el registro quedó sin usar, y bórralo.

La búsqueda del paso 0, repetida, tiene que devolver solo los archivos que vas a borrar. Si devuelve algo más, vuelve al paso 2.

Paso 4 — Colapsa la jerarquía y convierte en funciones.

Los pasos 4 y 5 de la lección 6.

Paso 5 — Decide qué hacer con lo que quedó colgando.

Tres cosas: la columna events.seating_plugin, el campo del formulario, y los cuatro tests del andamiaje. Las tres tienen respuestas distintas y las tres van justificadas en el documento. Recuerda la regla: el código es reversible; los datos no.

Paso 6 — Escribe el documento.

Con las cuatro secciones del ejemplo trabajado.

Qué se entrega

Cuatro cosas.

Uno: el antes y el después del código. Un diff, o las dos versiones de la carpeta. Con el comportamiento observable idéntico, demostrado por los tests que escribiste en el paso 1.

Dos: el inventario del costo, en una tabla con números. Archivos, líneas, líneas de trabajo real, saltos para responder una pregunta concreta, conceptos privados del proyecto, tests que prueban el andamiaje, implementaciones existentes, e incidentes atribuibles.

Tres: el documento de justificación, con estas cuatro secciones:

  • El contexto, en dos líneas. Qué es Boletia, quién la despliega, quién depende de este código. Sin esto, nada de lo que sigue se puede evaluar.
  • Qué costaba. Números, no adjetivos.
  • Qué se gana. Y sé honesto también con lo que se pierde: quitar el registro acopla el checkout a la función de asientos. Decirlo tú, antes de que te lo digan, es lo que demuestra que entendiste la lección 7.
  • Bajo qué condición volvería a ponerse. Verificable el lunes, y en dos niveles: 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. La lista de tus commits en orden, y qué verificaste después de cada uno. Es corta y demuestra la propiedad más importante del procedimiento: que en ningún momento el sistema estuvo roto.

Cómo se evalúa

No hay porcentajes. Hay siete cosas que se miran, y las siete son binarias.

El comportamiento observable no cambió. Y lo demuestras con tests escritos antes de tocar nada. Un test escrito 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 compila o no pasa los tests, el procedimiento no se siguió.

Encontraste los cinco usuarios. No solo el del checkout. Y resolviste el cuarto —el panel— con una decisión justificada, no ignorándolo.

No borraste la columna en el mismo cambio. Si tu entrega incluye una migración destructiva junto con la eliminación de código, eso es un fallo aunque todo lo demás esté perfecto. Es el error de riesgo de la lección 6.

La justificación usa números. Si tu documento dice "estaba sobre-diseñado" y no dice ciento ochenta y tres líneas, cero implementaciones en dos años y seis saltos, no es una justificación: es una opinión.

La condición de reversión es verificable. Alguien tiene que poder abrir el roadmap, o el contrato, o el código, y decir sí o no. "Si en el futuro hay más plugins" no cuenta.

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. Se siente incorrecto entregar un cambio que solo borra. Y así aparecen desmontajes que reemplazan el registro de plugins por… una fábrica. O por una clase SeatingService con un método. O por una interfaz "más simple, de dos métodos en vez de cinco".

Todas esas 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.

Si al terminar tu desmontaje sigue existiendo un concepto privado del proyecto para asignar asientos, vuelve al paso 4.

Errores comunes

Empezar por borrar (de método). Qué pasa: alguien abre 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 hizo que durara 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. Cómo detectarlo: si después de tu primer commit no puedes correr los tests, 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.

Ignorar el usuario 4 (de exhaustividad). Qué pasa: alguien hace un grep de get_for y get_seating_plugin, encuentra tres usuarios, los corta, borra el registro… y rompe el formulario de creación de eventos, porque el panel no llamaba a get_for: importaba _REGISTRY directamente. El fallo aparece días después, cuando alguien de operaciones intenta publicar un evento. Por qué pasa: se busca por el nombre de la función pública y no por el del módulo. Y porque _REGISTRY empieza con guion bajo, que en Python significa "privado", así que uno asume que nadie de fuera lo usa. Esa asunción es exactamente lo que hace peligrosas a las variables privadas usadas desde fuera: nadie las busca. Cómo detectarlo: busca por módulo, no por símbolo — grep -rn "plugins\." boletia/ encuentra los cinco. Cómo corregirlo: haz la búsqueda por módulo siempre, y antes de borrar cualquier archivo, busca su nombre. Y en tu documento, di qué hiciste con el desplegable: la respuesta honesta es que un campo de formulario con una sola opción no es una elección, así que se quita del formulario, se deja de escribir la columna, y la columna se borra después en una ventana aparte.

Entregar el código sin el documento, o el documento con adjetivos (de comunicación). Qué pasa: alguien hace un desmontaje técnicamente perfecto y lo entrega con una descripción de una línea: "limpieza del módulo de plugins". Quien revisa no tiene forma de evaluar si fue una buena idea, así que evalúa lo único que puede: el riesgo. Y un cambio que borra ciento ochenta y tres líneas del camino del checkout, sin justificación, es un cambio de riesgo alto. Se rechaza, o se queda meses sin revisar. Por qué pasa: el trabajo se siente terminado cuando el código funciona, y escribir la justificación se siente como burocracia. Cómo detectarlo: si tu descripción no contiene ni un número, no está lista. Cómo corregirlo: escribe las cuatro secciones. Te van a tomar quince minutos y son la diferencia entre un cambio que se aprueba en un día y uno que se discute tres semanas. El módulo 7 y el 8 de esta guía se dedican a esta habilidad; aquí la practicas por primera vez.

Ejercicios

Ejercicio 1 — Revisa un desmontaje ajeno. Un compañero entrega su trabajo con esta bitácora. Encuentra los tres 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  Migración: elimina la columna events.seating_plugin
commit 5  Agrega tests para SeatingService
Ver solución

Fallo 1 — el orden está invertido (commits 1 y 2). Borró primero y arregló después. Entre el commit 1 y el 2 el sistema no arranca: el checkout, el endpoint del mapa y la tarea programada importan módulos que ya no existen. Eso viola la regla del procedimiento —cada paso deja el sistema verde— y además significa que si el commit 2 hubiera salido mal, no había ningún punto intermedio al que volver. El orden correcto era cortar el uso primero, en cuatro commits separados, y borrar al final.

Y hay un daño colateral que se ve al mirar el commit 5: los tests están al final. No hubo red en ningún momento. El commit 5 prueba SeatingService, es decir, prueba lo que él escribió, no lo que había. Si el comportamiento cambió en el camino —por ejemplo, si el orden de asignación dejó de ser alfabético— los tests nuevos lo confirmarían como correcto.

Fallo 2 — sobre-corrección (commit 3). Reemplazó el registro por una clase SeatingService con tres métodos. Eso es la misma abstracción, más pequeña. Sigue habiendo un concepto privado del proyecto que alguien tiene que aprender, sigue habiendo un salto extra, y la clase no tiene estado: es un espacio de nombres disfrazado. El paso 5 del procedimiento existe exactamente para esto. La respuesta correcta era seating/seating.py con tres funciones.

Y falta la búsqueda: en ningún commit aparece nada sobre admin/panel.py. Si el panel importaba _REGISTRY, el formulario de eventos está roto desde el commit 1 y nadie lo sabe todavía.

Fallo 3 — la migración destructiva (commit 4). Borra la columna en el mismo trabajo, y además antes de haber confirmado que nadie la lee. Si el reporte de eventos del área de operaciones la consultaba, el dato ya no existe y revertir el despliegue no lo trae de vuelta. El camino correcto son tres tiempos separados por días: dejar de escribirla, confirmar que nadie la lee, borrarla en una ventana aparte con respaldo.

Por qué funciona: revisar el trabajo ajeno es la mejor forma de calibrar el propio, y los tres fallos de esta bitácora son los tres errores comunes de la lección en su forma más pura. Si los encontraste sin mirar la solución, tu propio desmontaje va a salir bien.

Ejercicio 2 — La variante que cambia la respuesta. Rehaz el análisis suponiendo que existe este documento: un contrato firmado con el Teatro Metropolitan, con fecha de 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? Contesta con los tres ejes de la lección 7 y di qué harías con el rincón plugins/.

Ver solución

Eje 1 — probabilidad del cambio: de bajo a máximo. Ya no es una predicción: es una obligación con fecha. La regla de tres tiene una excepción explícita para esto —cuando el tercer caso ya está firmado, no imaginado— y aquí se cumple con documento.

Eje 2 — costo del cambio si llega: de bajo a muy alto. El código lo va a escribir gente fuera de tu equipo, en su infraestructura, y tú no puedes desplegar por ellos. Ese es el disparador del eje: cuando hay que coordinar con terceros, el costo deja de ser trabajo y pasa a ser negociación.

Eje 3 — lectores: sube también. La interfaz pasa a ser una superficie pública: la va a leer el equipo del Teatro y probablemente la de otros recintos después. Cambiarla más adelante deja de ser un refactor.

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

  • 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, algo para reservar temporalmente mientras el cliente paga—.
  • El supports() que devuelve True siempre se vuelve peligroso de verdad: con dos plugins registrados, la iteración sobre un diccionario decidiría cuál gana. Eso hay que resolverlo 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 y 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.

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. Esa es la lección 7 aplicada, y es la razón por la que la primera sección de tu documento es el contexto: sin él, nadie puede evaluar el resto.

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

Ejercicio 3 — El puente al módulo 3. Deja plugins/ un momento y mira el otro rincón de Boletia: pricing/. Hay cuatro tipos de boleto —general, VIP, early-bird y cortesía— resueltos hoy con condicionales repartidos por el código, porque Ticket.kind es un texto libre. Contesta con las herramientas de este módulo: (a) ¿en qué dirección va este rincón, quitar o agregar estructura? (b) ¿qué evidencia lo sostiene? (c) ¿en qué peldaño del espectro pondrías la solución? (d) ¿qué te falta saber, y que este módulo no te enseñó, para hacerlo bien?

Ver solución

(a) Agregar. Es la dirección contraria a la de este proyecto, y por eso el ejercicio existe: el criterio que construiste no es antiabstracción, es de proporción.

(b) La evidencia, con las herramientas del módulo:

  • La pregunta de bolsillo de la lección 1: ¿cuántas implementaciones existen hoy? Cuatro, y son de verdad distintas —una multiplica, otra depende de una fecha de corte, otra es precio cero con un tope de emisiones—. Muy por encima del umbral de la regla de tres.
  • El eje 1 de la lección 7: cambió cinco veces en el último año, porque marketing hace promociones. Alto, con evidencia del historial.
  • El eje 2: es dinero y está en el camino del checkout. Un error cobra de más o de menos a un cliente real.
  • El eje 3: es de los rincones que más se leen, porque cada promoción nueva obliga a entenderlo.
  • Y el costo actual de la duplicación no es cosmético: los condicionales sobre Ticket.kind están repartidos, así que agregar un tipo de boleto obliga a encontrarlos todos, y olvidarse de uno cobra mal.

(c) Peldaño 5: una interfaz y un diccionario que mapee tipo de boleto a regla. No peldaño 7. No hace falta ningún registro dinámico: las cuatro reglas las escribe tu equipo, en tu repositorio, y la lista cabe en cinco líneas visibles. Nota que este es exactamente el mismo peldaño que recomendarías para el modo de grupos del encargo del principio.

(d) Lo que falta: cómo se llama esa estructura, cuáles son sus variantes, y cómo distinguirla de sus parientes. Este módulo te enseñó a decidir si hace falta una abstracción; no te enseñó cuál. Sabes que aquí cabe algo con cuatro implementaciones intercambiables, pero no sabes todavía si lo correcto es Strategy —comportamientos intercambiables—, Template Method —mismo esqueleto, un paso distinto— o simplemente cuatro funciones en un diccionario, que en Python muchas veces le gana a las tres clases.

Por qué funciona: el ejercicio cierra el módulo mostrando que lo que aprendiste es un criterio de dos direcciones, y abre el siguiente dejándote con una pregunta concreta que el módulo 3 responde. Que la pregunta te quede abierta y precisa es exactamente el estado en el que conviene llegar a la siguiente lección.

Resumen y siguiente paso

Con este proyecto cierras el módulo que define esta guía.

Tomaste una abstracción real —cinco archivos, ciento ochenta y tres líneas, cinco usuarios, una implementación— y la desmontaste hasta dejar el código directo que hace exactamente lo mismo, en pasos que nunca dejaron el sistema roto. Encontraste el usuario que se esconde detrás de una variable privada, decidiste qué hacer con un campo de formulario que fingía ser una elección, y separaste lo que se puede revertir de lo que no.

Y escribiste el documento, que es la mitad del trabajo: el contexto en dos líneas, el costo en números, lo que se gana y lo que se pierde, y la condición verificable bajo la cual volverías a poner el mecanismo. Esa última sección es la que convierte una opinión en criterio, y es la que te van a pedir en cada decisión de diseño del resto de tu carrera.

Mira hacia atrás un momento. Empezaste el módulo con una intuición —"a veces se abstrae de más"— y sales con siete herramientas concretas: los cuatro costos de una capa, la regla de tres con su eje de variación, la comparación honesta entre duplicar y abstraer mal, la prueba de las tres preguntas de YAGNI, las nueve señales del olor con sus cinco excepciones legítimas, el espectro de siete peldaños, y los tres ejes de la decisión. Ninguna de ellas es una regla. Todas son preguntas con respuestas verificables. No está mal para un módulo que todavía no te enseñó un solo patrón.

Y esa es justamente la siguiente parada. El módulo 3 empieza a enseñar patrones concretos, y empieza por el grupo más útil de todos: los que se ocupan del comportamiento que varía. Vas a ver Strategy, Template Method y State, aplicados a los rincones de Boletia donde una abstracción sí se gana su lugar —empezando por pricing/, que acabas de diagnosticar en el último ejercicio—. Vas a ver también cuándo una función de primera clase le gana a una clase, y la trampa que este módulo te preparó para reconocer: que no todo condicional es una Strategy escondida.

La diferencia es que ahora llegas con el criterio puesto. Cuando aparezca Strategy, tu primera reacción no va a ser buscar dónde meterla. Va a ser preguntar cuántas implementaciones hay hoy, mirar el historial, y ubicar el peldaño más bajo que resuelva el problema. Ese orden —criterio primero, técnica después— es lo que separa a quien aplica patrones de quien decide sobre ellos.

Recursos