Módulo 6: Patrones para comunicar entre partes
8. Proyecto: desacopla la notificación de la compra
Descripción
Al terminar este proyecto vas a haber hecho lo que el módulo entero venía preparando: tomar el checkout de Boletia con sus ocho reacciones y decidir, una por una, cuál merece ser un evento, cuál se queda como llamada directa y cuál va al punto medio. No es un ejercicio de implementar Observer. Es un ejercicio de decidir, y el código es la consecuencia de las decisiones, no al revés.
Vas a entregar tres cosas. Una tabla de decisiones —ocho filas, una por reacción, con la señal que la justifica—, el código refactorizado, y el registro de qué escucha qué, que es la pieza que evita que el sistema pierda la trazabilidad de la que habla la lección 6. Y una cuarta opcional que te recomiendo mucho: la nota de lo que no harías y por qué.
Lo importante de este proyecto es lo que se evalúa. No se juzga por cuántos eventos introdujiste. Una entrega con dos eventos y seis decisiones bien argumentadas vale más que una con ocho eventos y una justificación de una línea. De hecho, hay una entrega perfectamente válida que introduce cero eventos, si la defensa está bien hecha. Lo que se juzga es si cada decisión tiene un argumento verificable detrás, y si el conjunto es coherente con las garantías que el negocio necesita.
Conexión con el módulo: este proyecto usa todo. El diagnóstico de la lección 1 —"si esto falla, ¿la compra falló?"—. El mecanismo de la lección 2. El caso trabajado de la lección 3, incluida su conclusión incómoda sobre el inventario. Las reglas de diseño de eventos de la lección 4. Command y las colas de la lección 5. Las mitigaciones de trazabilidad de la lección 6. Y, sobre todo, las doce señales de la lección 7, que son la herramienta con la que vas a defender cada fila de tu tabla. Es el cierre del módulo y la antesala del 7, donde esas justificaciones se convierten en el vocabulario con el que se conversa en una revisión.
El encargo, tal como llega
Un martes cualquiera, en el canal del equipo:
"Necesitamos poder agregar cosas que pasen después de una compra sin tener que tocar el checkout cada vez. La semana pasada, por meter los puntos de lealtad, tuvimos que parar un despliegue porque el cambio rompió la venta en pruebas. Y ayer se cayó el servicio de facturación cuarenta minutos y dejamos de vender, cuando la factura ni siquiera es urgente. Haz lo que tengas que hacer, pero explícanos qué cambia y cómo lo depuramos cuando falle."
Léelo con atención, porque está mejor escrito de lo que parece y contiene tus tres requisitos:
- Agregar reacciones sin tocar el
checkout. Es la ganancia que se busca. - Que una reacción no urgente no tumbe la venta. Es el incidente concreto que motivó el pedido, y es un requisito de garantías, no de elegancia.
- "Explícanos qué cambia y cómo lo depuramos cuando falle." Es lo que separa un entregable profesional de un refactor suelto, y es la lección 6 pedida explícitamente por quien va a vivir con el resultado.
Fíjate en lo que el encargo no dice: no menciona ningún patrón, no pide un bus de eventos y no dice "desacopla". Dice qué duele. Traducir un dolor a una decisión de diseño —y no al revés— es la habilidad que este proyecto entrena.
El código sobre el que trabajas
Esta es la sección 4 del checkout de Boletia hoy, completa, con las ocho reacciones numeradas. Los comentarios de fecha son reales: cada una entró en un momento distinto, por un motivo distinto y por una persona distinta.
# ============================================================
# checkout/checkout.py — la sección 4, hoy
# ============================================================
def checkout(order, coupon=None):
# ... secciones 1 a 3: precio, asientos, cobro ...
# Al llegar aquí, el cobro ya ocurrió y result.ok es True.
order.status = "paid"
repository.save_order(order)
# ---- 4. Todo lo que pasa después de cobrar -------------------------
customer = repository.get_customer(order.customer_id)
event = repository.get_event(tickets[0].event_id)
# (1) Confirmación al comprador. Desde el principio del producto.
email_channel.send(customer.email, build_confirmation(order))
if customer.phone:
sms_channel.send(customer.phone, build_short_confirmation(order))
if customer.push_token:
push_channel.send(customer.push_token, build_push_confirmation(order))
# (2) Aviso al organizador. Solo correo: tiene teléfono cargado y no
# queremos mandarle un SMS por cada boleto de un festival.
organizer = repository.get_customer(event.organizer_id)
email_channel.send(organizer.email, build_organizer_alert(order, event))
# (3) Inventario. Agregado en marzo: dejó de cuadrar porque nadie
# marcaba los boletos.
for ticket in tickets:
ticket.status = "sold"
repository.save_ticket(ticket)
inventory.decrement_available(event.id, len(tickets))
# (4) Reserva temporal. Al entrar al checkout se pone un "hold" de 10
# minutos sobre los asientos; aquí se libera porque ya son del
# comprador. Si no se libera, el asiento queda bloqueado y nadie
# más lo puede comprar hasta que expire.
for ticket in tickets:
holds.release(ticket.id, order.id)
# (5) Factura. Agregada en mayo por contabilidad.
if order.total > 0:
invoice = billing.create_invoice(order, customer)
email_channel.send(customer.email, build_invoice_email(invoice))
# (6) Analítica y panel del organizador. Junio, equipo de datos.
analytics.track("order_paid", order_id=order.id, total=order.total)
reports.refresh_event_dashboard(event.id)
# (7) Puntos de lealtad. Hace dos semanas, marketing.
loyalty.add_points(customer.id, points=int(order.total // 10))
# (8) Webhook del organizador. Ayer. Algunos organizadores configuran
# una URL propia para engancharse a sus ventas. Es HTTP a un
# servidor de terceros: puede tardar, puede fallar, puede no existir.
if event.webhook_url:
requests.post(event.webhook_url, json=serialize_order(order), timeout=10)
return order
# ============================================================
# notifications/ — lo que ya existe (módulo 1)
# ============================================================
# channel.py → NotificationChannel: send() + is_available_for()
# email/sms/push → tres implementaciones. sms y push devuelven False en
# is_available_for() cuando el Customer no tiene phone
# o push_token.
# retrying.py → RetryingChannel: envuelve un canal y reintenta 3 veces
# con 2 segundos de espera. Solo errores transitorios.
# notifier.py → CHANNELS = [RetryingChannel(EmailChannel()),
# RetryingChannel(SmsChannel()),
# PushChannel()]
# notify(customer, message): recorre CHANNELS y manda
# por los que apliquen al cliente.
# manager.py → NotificationManager: 6 métodos sin relación. Nadie lo usa.
#
# ⚠️ notify() existe desde hace un año y el checkout NO lo usa. Lo usa solo
# el script de recordatorios. Es la migración a medias del módulo 1.
# ============================================================
# Datos útiles para decidir
# ============================================================
# - Customer.phone y Customer.push_token pueden ser None.
# - El equipo es de 6 personas. Notificaciones, facturación y datos las
# toca gente distinta, pero todos pueden tocar todo.
# - En el último año, checkout.py se modificó 11 veces: 4 por razones del
# checkout (cobro, cupones) y 7 por agregar o ajustar reacciones.
# - El festival grande vende ~4,000 boletos en las 2 horas siguientes al
# anuncio. En ese pico, el checkout tarda hoy 1.8 segundos en promedio.
# - El servicio de facturación se cayó 40 minutos ayer.
# - Tres organizadores tienen webhook configurado. Uno de ellos tiene un
# servidor que tarda 8 segundos en responder.
Ejemplo trabajado: una decisión completa, para que veas el nivel
Antes de que lo hagas tú, voy a decidir una de las ocho y escribirla completa. Fíjate en el formato, en el uso de las señales y sobre todo en que la justificación menciona una consecuencia concreta, no una propiedad abstracta.
Reacción (8) — Webhook del organizador
- Qué hace hoy: una llamada HTTP síncrona a un servidor de terceros, con diez segundos de tiempo límite, dentro del flujo de la compra.
- ¿Si falla, la compra falló? No. El comprador tiene su boleto, su dinero salió y su asiento es suyo. El webhook es un servicio adicional para el organizador.
- Señales que aplican: la 4 con mucha fuerza (no debe bloquear la operación: hoy un organizador con un servidor lento le agrega ocho segundos a la compra de sus clientes) y la 5 (el publicador no debería conocer a los interesados: la URL la pone un tercero y puede haber muchas). En contra no aplica ninguna: no necesitamos el resultado, el orden no importa, no es la misma transacción.
- Decisión: evento, y además el manejador encola un comando.
- Por qué las dos cosas: el evento lo saca del
checkout; el comando en cola resuelve un problema distinto y también real. Con solo el evento y despacho síncrono, el organizador lento sigue agregando ocho segundos a la venta, ahora escondidos detrás de unpublish. Encolar es lo que corta de verdad esa dependencia temporal, y además permite reintentar cuando el servidor del organizador está caído — que es lo normal en servidores de terceros. - Qué se paga: un trabajo más en la cola por venta con webhook (tres organizadores, así que poco volumen); la posibilidad de entrega duplicada, que aquí es aceptable porque el contrato del webhook es "al menos una vez" y así se documenta para los organizadores; y un lugar más donde mirar cuando un organizador reclame que no le llegó nada.
- Cómo se depura: el comando
CallOrganizerWebhook(event_id, order_id, url, trace_id)queda guardado con su identificador de traza. Ante un reclamo, se busca pororder_iden la tabla de trabajos y se ve el estado, los intentos y el último error. Es más fácil de depurar que hoy, porque hoy no queda ningún rastro de un webhook fallido más allá de una línea de excepción.
Fíjate en cinco cosas de esa entrada, porque son lo que quiero ver en las tuyas.
Empieza por la pregunta de las garantías, no por el patrón. "¿Si falla, la compra falló?" es la primera línea porque es la que decide.
Nombra las señales que aplican y también las que no. Decir "no aplica ninguna en contra" es información: significa que revisaste las seis.
La justificación menciona un número real: ocho segundos, tres organizadores. Los números del enunciado están ahí para que los uses. Una justificación sin números es una impresión.
Dice qué se paga. Toda decisión tiene costo; una entrega que solo enumera beneficios no está evaluando, está vendiendo.
Contesta cómo se depura. Es el tercer requisito del encargo y es lo que más se olvida. Nota además el detalle: en este caso el diseño nuevo es más fácil de depurar que el actual. Cuando eso pasa, dilo — es el argumento más fuerte que vas a tener.
El proyecto: tres entregas obligatorias y una recomendada
Entrega 1 — La tabla de decisiones (obligatoria)
Ocho filas, una por reacción. Para cada una:
| Columna | Qué va |
|---|---|
| Reacción | El número y una descripción de tres palabras |
| ¿Si falla, la compra falló? | Sí / No, y en una línea por qué |
| Decisión | Se queda dentro · Función agrupada · Evento · Evento + comando en cola |
| Señales | Cuáles de las doce de la lección 7 aplican, a favor y en contra |
| Qué se paga | El costo concreto de tu decisión |
Reglas de la tabla:
- Ninguna fila puede tener la misma justificación que otra. Si dos filas dicen lo mismo, evaluaste un caso y lo copiaste. Las ocho reacciones tienen propiedades distintas; encuéntralas.
- Al menos una reacción tiene que quedarse dentro del
checkout. Si tu tabla saca las ocho, revísala: hay al menos una que no puede salir sin romper una garantía del negocio, y encontrarla es la mitad del ejercicio. - Usa los números del enunciado. Las once modificaciones del último año, los 1.8 segundos, los ocho segundos del organizador lento, los cuarenta minutos de facturación caída. Están puestos a propósito.
Entrega 2 — El código (obligatoria)
Escribe el resultado de tus decisiones. Lo mínimo:
- La sección 4 del
checkoutcomo te queda. Es la prueba visible de tus decisiones. - Los archivos nuevos que hayan hecho falta. Si decidiste eventos:
bus/bus.py,bus/events.py,bus/wiring.pyy los manejadores. Si decidiste la función agrupada: ese archivo. Si decidiste una cola: el comando y el esqueleto del ejecutor. - La definición de cada evento que introduzcas, aplicando las siete reglas de la lección 4. Se revisa el nombre (¿pasado?, ¿del negocio?), los campos (¿identificadores?, ¿unidades explícitas?) y la inmutabilidad.
- La política de errores, escrita en código. Qué pasa si una reacción falla. No basta con decirlo en prosa: tiene que verse en el
publish, en el_safelyo donde vaya.
No hace falta que corra. Es un ejercicio de diseño, no de entorno. Pero tiene que ser código que podría correr: sin pseudocódigo, sin # aquí va la lógica.
Entrega 3 — El registro de qué escucha qué (obligatoria)
Esta entrega es la que hace que el proyecto sea de este módulo y no de otro. El encargo dijo "explícanos cómo lo depuramos cuando falle", y esto es la respuesta.
Entrega las dos partes:
(a) El mapa. Un archivo docs/event-map.txt —o el nombre que prefieras— con el cableado completo: cada evento, cada suscriptor con su módulo, y una línea de qué hace. Si tu diseño no tiene eventos, entrega el equivalente: la lista de las reacciones con su hogar y su orden.
(b) El mecanismo que impide que ese mapa mienta. El describe() que lo genera desde el sistema vivo, la prueba que lo compara, o los dos. Si tu mapa es un documento escrito a mano y nada lo verifica, la entrega está incompleta: la lección 6 fue explícita en que ese documento va a mentir dentro de dos meses.
Y agrega un párrafo corto: cómo se diagnostica un aviso que no llegó, en pasos, con tu diseño. Cuatro o cinco pasos, del tipo "corro esto, busco aquello". Si al escribirlo notas que son ocho pasos y dos suposiciones, tu diseño necesita más mitigaciones o menos indirección — y darte cuenta de eso ahora es el objetivo del ejercicio.
Entrega 4 — Lo que no harías (opcional, muy recomendada)
Media página. Tres cosas que consideraste y descartaste, con la razón. Por ejemplo: por qué no encolaste todo, por qué no publicaste un evento por cada reacción, por qué no le pusiste versionado a los eventos, por qué no metiste el Customer completo en el evento aunque habría ahorrado consultas.
Esta parte es opcional y es la que más te va a servir. En una entrevista, y en una revisión de código, la pregunta que separa a quien aplicó una receta de quien tomó una decisión es siempre la misma: "¿qué otras opciones evaluaste?". Tener la respuesta escrita es tenerla lista.
Los tres caminos defendibles
Para que no busques la respuesta correcta: hay tres entregas muy distintas que yo aprobaría, y la diferencia entre ellas es cuánto pesas cada costo. Lo que no aprobaría es cualquiera de las tres sin justificación.
Camino A — El conservador. Cero eventos. Todas las reacciones que pueden salir se van a checkout/after_purchase.py, con aislamiento de errores; las transaccionales se quedan dentro; el webhook se encola porque es lo único que de verdad no puede correr síncrono. Argumento: seis personas, un solo despliegue, y las mitigaciones de la lección 6 cuestan más que el problema que resuelven. Es un camino completamente defendible y probablemente el que más rápido resuelve el dolor del encargo. Su punto débil está en el dato de las once modificaciones: siete de once fueron por reacciones, y este camino no elimina del todo esa presión, solo la mueve a un archivo menos peligroso.
Camino B — El intermedio. Un evento, OrderCompleted, con las reacciones opcionales como suscriptores; las transaccionales dentro del checkout; el webhook como suscriptor que encola un comando. Con describe(), mapa verificado y trazas. Es el diseño que construimos en las lecciones 3 a 6 y es el que mejor responde al encargo completo. Su costo es el de aprendizaje del equipo y el mapa que hay que mantener.
Camino C — El de crecimiento. Como el B, más una segunda decisión: encolar también las notificaciones, no solo el webhook. Argumento: el pico de 4,000 boletos en dos horas y los 1.8 segundos actuales; sacar los envíos del camino crítico baja el tiempo de respuesta y hace que un proveedor de correo caído no pierda confirmaciones —el problema del RetryingChannel de seis segundos de la lección 5—. Su costo es el más alto: una cola, un proceso más que vigilar y la exigencia de idempotencia en cada comando.
Los tres son buenas entregas. La mala entrega es la que elige uno sin poder decir por qué no eligió los otros dos.
Cómo saber si tu entrega es buena
Antes de darla por cerrada, pásala por estas seis preguntas. Son las mismas con las que yo la revisaría.
1. ¿Alguna reacción se quedó dentro del checkout, y puedes decir por qué en una frase? Si las ocho salieron, hay al menos una garantía rota. La frase tiene que hablar de una consecuencia concreta —dos personas en el mismo asiento, un asiento bloqueado que nadie puede comprar—, no de una propiedad abstracta.
2. ¿Tus ocho justificaciones son distintas entre sí? Si tres filas dicen "no bloquea la operación principal", esas tres reacciones probablemente sí sean parecidas, pero las otras cinco tienen que tener razones propias.
3. ¿Cada evento que introdujiste cumple las siete reglas de la lección 4? Nombre en pasado y del negocio, identificadores en vez de objetos, inmutable, campos con unidad y moneda explícitas, y un solo lugar donde viven las definiciones.
4. ¿Puedes contestar "¿qué pasa cuando se completa una compra?" abriendo un archivo o corriendo un comando? Si la respuesta exige una búsqueda por el repositorio o "depende", la entrega 3 no está resuelta.
5. ¿Tu política de errores está escrita en código y es coherente? Aislar los errores de una reacción opcional es correcto; aislar los de una transaccional es un desastre. Si tu diseño usa el mismo try/except para las dos categorías, revísalo.
6. ¿Mencionaste al menos un costo por cada decisión? Una tabla que solo tiene beneficios no es una evaluación.
Y la pregunta de control, la que resume todo el módulo: si dentro de un año alguien quiere quitar lo que hiciste, ¿va a poder averiguar quién escucha qué? Si la respuesta es sí, tu diseño respeta la asimetría de la lección 7. Si es no, construiste algo que nadie va a poder desmontar, y eso es una decisión que le impusiste al futuro sin su permiso.
De Boletia a un repositorio de verdad
Cuando quieras probar esto fuera de la guía, el ejercicio se traslada casi tal cual:
Busca la función más larga de un sistema que conozcas —tuyo, del trabajo, de un proyecto de código abierto— y mira su último tercio. En una cantidad sorprendente de sistemas, el final de la función principal es exactamente esto: una lista de cosas que pasan después. Correo, métrica, caché que se invalida, registro de auditoría, aviso a otro servicio.
Corre el historial sobre ese archivo. git log --oneline y cuenta cuántas de las últimas veinte modificaciones fueron por la razón principal del archivo y cuántas por agregar una reacción. Esa proporción es el dato más honesto que vas a conseguir sobre si el evento se gana su lugar, y es exactamente el dato que Boletia te dio servido —siete de once—.
Aplica la pregunta de las garantías a cada línea. Vas a encontrar, casi seguro, al menos una que está mezclada: algo que es parte de la operación conviviendo con avisos, en el mismo bloque y con el mismo manejo de errores. Encontrar esa línea, aunque no la toques, ya vale el ejercicio.
Y mira si hay una migración a medias. El notify() que existe y nadie usa no es un invento didáctico: es la cosa más común que hay en sistemas de más de tres años. Alguien empezó a hacer las cosas de una forma nueva, no terminó, y ahora conviven dos caminos. Casi siempre, la mejor manera de empezar un refactor de este tipo es terminar la migración que alguien dejó a medias, en vez de inventar una tercera forma.
Errores comunes
Empezar por el código (de criterio). Qué pasa: se abre el editor, se escribe el EventBus, y las decisiones se van tomando sobre la marcha, en el orden en que aparecen las líneas. El resultado suele ser homogéneo —todo evento o todo dentro— porque una vez que la maquinaria está escrita, usarla es el camino de menor resistencia. Por qué pasa: escribir código se siente productivo y llenar una tabla se siente burocrático. Cómo detectarlo: si tu tabla de decisiones la escribiste después del código, la escribiste para justificar lo que ya habías hecho. Cómo corregirlo: la tabla primero, completa, y recién entonces el editor. Vas a notar que dos o tres decisiones cambian al escribirlas, y ese cambio es exactamente el valor del ejercicio.
Tratar las ocho reacciones como una sola cosa (de criterio). Qué pasa: la entrega dice "las reacciones a la compra se convierten en suscriptores de OrderCompleted" y ya. Ocho decisiones colapsadas en una. Por qué pasa: porque el bloque se ve homogéneo —todo está junto, en la misma función, después de cobrar— y esa apariencia es engañosa. Cómo detectarlo: cuenta cuántas veces aparece la palabra "todas" en tu justificación. Cómo corregirlo: obliga a la tabla a tener ocho filas con ocho razones distintas. La liberación del hold y el aviso al organizador no se parecen en nada más que en el lugar donde están escritos.
Entregar el mapa sin el mecanismo que lo mantiene honesto (conceptual). Qué pasa: se entrega un docs/event-map.md precioso, hecho a mano, y nada más. A los dos meses miente. Por qué pasa: porque el documento resuelve el pedido literal —"explícanos cómo lo depuramos"— y el mecanismo parece un extra. Cómo detectarlo: pregúntate qué tendría que pasar para que ese archivo quede desactualizado sin que nadie se entere. Si la respuesta es "que alguien tenga prisa", el mecanismo falta. Cómo corregirlo: el describe() y la prueba que lo compara. Doce líneas, y son la diferencia entre documentación que sirve y documentación que engaña.
Ejercicios
Estos tres se hacen después de entregar, y sirven para probar tu propio diseño.
Ejercicio 1 — El noveno interesado. Llega el pedido: "cuando alguien compre boletos VIP, hay que avisarle al equipo de atención para que le llame antes del evento". Con tu diseño en la mano, escribe qué archivos tocas y estima el tiempo. Después escribe qué habrías tocado con el código original.
Ver solución
Con el código original: checkout/checkout.py, tres líneas —el if que detecta VIP más la llamada— y revisión de quien cuida el checkout. Media hora de trabajo y probablemente un día de espera por la revisión, más el riesgo de tocar el archivo del que depende la venta.
Con el camino A (función agrupada): checkout/after_purchase.py, una línea, más el archivo del manejador. Media hora, revisión de bajo riesgo. El checkout no se toca.
Con el camino B o C (eventos): un archivo nuevo en support/subscribers.py, una línea en wiring.py, y regenerar el mapa. Media hora también, y el checkout no se toca.
El hallazgo que quiero que veas: para este cambio concreto, A, B y C cuestan prácticamente lo mismo. La diferencia entre la función agrupada y el bus de eventos no aparece con el noveno interesado: aparece cuando dos equipos distintos quieren tocar el archivo de reacciones al mismo tiempo, o cuando alguien de fuera del repositorio necesita engancharse. Si tu justificación del camino B se apoyaba en "es más fácil agregar interesados", este ejercicio te acaba de mostrar que ese argumento es más débil de lo que parecía, y que el argumento bueno era otro: el de la coordinación entre personas.
Hay un detalle extra que vale la pena notar. El if ticket.kind == "vip" tiene que vivir en algún lado. En el diseño con eventos vive en el manejador, que es lo correcto: es una regla de atención a clientes, no de compra. Pero fíjate en que el manejador necesita saber los tipos de boleto de la orden, y OrderCompleted solo lleva ticket_ids. O el manejador hace una consulta, o el evento crece. Esa es exactamente la tensión delgado/gordo de la lección 4, apareciendo en tu propio diseño.
Ejercicio 2 — El incidente. A las 3 de la mañana, un organizador reclama que no le llegó el aviso de sus últimas cincuenta ventas. Con tu diseño, escribe los pasos exactos del diagnóstico y cronométralos honestamente. Si pasan de diez minutos, di qué le falta a tu entrega 3.
Ver solución
Un diagnóstico de diez minutos con el camino B se ve así:
python -m bus.describe→ confirmo quesend_organizer_alertestá suscrita aOrderCompleted. 30 segundos.- Busco en el registro por
send_organizer_alerten las últimas horas. 1 minuto. - Encuentro
FALLÓ send_organizer_alert trace=7f3a9bconSMTPRecipientsRefused: 552 mailbox full. Causa raíz. 1 minuto. - Verifico con el identificador de traza que el resto del flujo de esa compra corrió bien, para saber si hay más daño. 2 minutos.
- Aviso al organizador y agrego una alerta para que un rebote permanente no vuelva a pasar desapercibido cincuenta veces. El resto.
Si tu diagnóstico pasa de diez minutos, lo más probable es que le falte una de estas tres cosas, en este orden de importancia:
- El identificador de traza en cada línea de registro. Sin él, sabes que algo falló pero no para quién ni cuántas veces. Es el que más tiempo ahorra.
- Un
describe()que salga del sistema vivo. Sin él, el paso 1 se convierte en una búsqueda por el repositorio con incertidumbre. - El mensaje de la excepción original en el registro. Si tu
exceptescribe solo "falló el manejador" y no la excepción completa, perdiste la causa raíz y el diagnóstico se vuelve una investigación.
Y una cosa más, que es de las que se aprenden viviéndolas: si tu respuesta al paso 3 fue "reviso el código para ver qué pudo fallar", tu diseño depende de que alguien razone en vez de leer. Un buen diagnóstico se lee, no se deduce.
Ejercicio 3 — Defiende tu entrega ante las tres críticas. Escribe tu respuesta, en cuatro líneas cada una, a: (a) "esto es sobre-ingeniería para un equipo de seis"; (b) "quedó inconsistente: unas reacciones son eventos y otras no"; (c) "antes yo leía una función y sabía todo lo que pasaba; ahora no".
Ver solución
(a) Sobre-ingeniería. Es la crítica más seria de las tres y merece datos, no defensa. Una respuesta que funciona: "El dato que me convenció son las once modificaciones del último año: siete fueron por reacciones, no por el checkout. Eso son siete veces que alguien tocó el archivo del que depende la venta por algo que no le pertenece, y una de esas veces paramos un despliegue. Si ese número fuera cero o uno, estaría de acuerdo contigo y habría dejado todo dentro." Fíjate en la última frase: nombra la condición bajo la cual la crítica tendría razón. Eso convierte una defensa en una conversación.
(b) Inconsistencia. "Es deliberada. Las que se quedan dentro —inventario y liberación del hold— tienen que fallar junto con la compra; si las saco, el sistema puede vender un asiento dos veces o dejarlo bloqueado sin que nadie se entere. Las que salieron pueden fallar sin invalidar la venta. La consistencia que perdemos es de forma; la que ganamos es de garantías, y esa es la que le importa al cliente que está en la puerta del teatro." Es la respuesta de la lección 3, y es la que más veces vas a tener que dar en tu carrera: una simetría que el dominio no tiene es una distinción que borraste.
(c) Ya no leo el flujo en una función. Esta crítica es cierta y la peor respuesta posible es negarla. "Tienes razón, y es el costo real del cambio. Lo compensé con tres cosas: python -m bus.describe te da el flujo completo en quince segundos, una prueba impide que ese mapa se desactualice, y cada línea de registro lleva un identificador de traza para que puedas seguir una compra de punta a punta. No es lo mismo que leer una función, y quiero que me digas en un mes si te alcanza." Reconocer el costo y mostrar la mitigación convence; discutir que el costo existe, no.
Por qué funciona: las tres críticas son las que de verdad te van a hacer, y las tres tienen algo de razón. La forma de responderlas no es tener un contraargumento, es haber tomado la decisión sabiendo que existían. Eso es lo que la tabla de decisiones te deja, y es la diferencia entre defender un diseño y defender tu ego.
Resumen y siguiente paso
En este proyecto hiciste el trabajo completo del módulo: tomaste ocho reacciones que se veían homogéneas y descubriste que tienen propiedades distintas; decidiste una por una con las doce señales de la lección 7; escribiste el código como consecuencia de esas decisiones y no al revés; y entregaste el registro de qué escucha qué con el mecanismo que impide que mienta.
Y viste que había tres caminos defendibles, no uno. El conservador sin eventos, el intermedio con un evento, y el de crecimiento con cola. Los tres resuelven el encargo. Lo que separa una buena entrega de una mala no es cuál elegiste: es si puedes decir por qué no elegiste los otros dos.
Antes de cerrar el módulo deberías poder: decidir si una reacción sale o se queda con una sola pregunta; nombrar las señales que sostienen cada decisión; escribir un evento que cumpla las siete reglas; y diagnosticar un aviso que no llegó en menos de diez minutos con tu propio diseño.
Ahora mira lo que acabas de hacer, porque es más de lo que parece. La entrega que más peso tenía no era el código: era la tabla y las justificaciones. Pasaste el módulo entero traduciendo estructuras a argumentos —"esto no puede salir porque dos personas terminan en el mismo asiento", "esto sí porque un servidor lento le agrega ocho segundos a la venta"— y esa traducción es, exactamente, de lo que trata el módulo 7.
Ahí los patrones dejan de ser cosas que implementas y pasan a ser vocabulario de revisión: la manera de nombrar en tres palabras lo que a otro le tomaría un párrafo, en una conversación con un compañero que no tiene por qué estar de acuerdo contigo. Vas a aprender el otro lado del diccionario —los nombres de lo que está mal: God object, feature envy, shotgun surgery— y, sobre todo, a nombrar un problema de forma que sea accionable y no una sentencia. Que es la diferencia entre una revisión que mejora el código y una que arruina la tarde de alguien.
Recursos
- Martin Fowler — What do you mean by "Event-Driven"? — vale la pena releerlo con tu tabla de decisiones en la mano: vas a reconocer cuál de sus cuatro sentidos usaste.
- Refactoring Guru — Observer y Command — como referencia de la forma canónica de los dos patrones del módulo.
- Celery — Tasks — si tu entrega incluyó una cola, su documentación sobre reintentos e idempotencia es el siguiente paso natural.
- The Grug Brained Developer — para leer después de entregar. Su tesis sobre la complejidad como enemigo principal es el criterio de este módulo contado como comedia, y se disfruta más cuando acabas de vivir la decisión.