Módulo 6: Patrones para comunicar entre partes
1. Presentación del módulo: quién le avisa a quién
Descripción
Al terminar esta lección vas a tener tres cosas. Primero, el problema del módulo visto con sus propias manos: el checkout de Boletia, que en el módulo 1 tenía cinco líneas de avisos al final, hoy tiene veinte, porque cada equipo que quiso enterarse de una compra fue a agregar su llamada ahí. Segundo, el eje que ordena todos los patrones de esta familia, que no es "qué varía" —ese fue el módulo 3— sino algo distinto: quién reacciona. Y tercero, la advertencia honesta: esta familia de patrones cobra un precio que casi ningún curso menciona, y es el más caro de todos los que hemos visto hasta ahora.
Esto importa porque el problema que vamos a atacar es, probablemente, el más universal del software de negocio. Piensa en cualquier sistema real: cuando pasa algo importante —se completó una compra, se registró un usuario, se canceló una reserva, se subió un archivo— casi nunca pasa una sola cosa después. Pasan cinco. Y esas cinco cosas suelen pertenecer a equipos distintos, a módulos distintos, y aparecer en momentos distintos de la historia del producto. La pregunta "¿quién le avisa a quién?" parece de plomería, pero decide la forma del sistema entero: si se contesta mal, terminas con una función que conoce medio código base y que cambia cada semana por razones ajenas.
Y hay algo más, que conviene decir de una vez. Los módulos 3, 4 y 5 te dieron patrones cuyo costo era local: si te equivocas con una Strategy, el daño se queda en un archivo o dos. El costo de esta familia es global: se paga cada vez que alguien intenta entender el sistema completo. Por eso el módulo tiene una lección entera —la 6— dedicada exclusivamente a ese costo, y por eso la lección 7 es una vacuna en toda regla. No es paranoia: es la parte del oficio que separa a quien aplica el patrón de quien decide sobre él.
Conexión con el módulo: esta lección es el planteamiento del problema; todavía no vas a ver ningún patrón implementado. La lección 2 presenta Observer con su anatomía completa y explica lo más importante: por qué invierte la dirección de la dependencia. La lección 3 lo aplica sobre el caso central del módulo, OrderCompleted con varios suscriptores, y mide exactamente qué se gana y qué se paga. La lección 4 desarma el malentendido más frecuente —creer que el acoplamiento desaparece— y enseña a diseñar un evento que no se rompa. La lección 5 trae el segundo patrón de la familia, Command, con su frontera honesta: casi siempre una función basta. La lección 6 es la más incómoda y la más valiosa: el flujo que ya no puedes seguir con el dedo. La lección 7 te da los criterios para decir "aquí no, aquí llamada directa". Y la lección 8 —el proyecto— te pide desacoplar el checkout decidiendo, caso por caso, cuáles reacciones merecen ser eventos y cuáles no.
La fiesta sorpresa y la lista de a quién avisar
Estás organizando una fiesta sorpresa para tu hermana. La fecha se movió al sábado y hay que avisar. Sacas el teléfono y empiezas: le escribes a tu mamá, a tu primo, a la amiga que lleva el pastel, al del sonido, al dueño del salón. Cinco mensajes, cinco conversaciones distintas, cinco veces la misma información escrita a mano.
La semana siguiente la fecha se mueve otra vez. Vuelves a la lista. Pero ahora resulta que se sumaron tres personas más, y que tu primo ya no viene, y que la amiga del pastel cambió de número. Tú eres la única persona que sabe la lista completa, así que tú eres el cuello de botella: si te olvidas de alguien, esa persona llega el viernes al salón vacío. Y lo peor no es el trabajo de mandar los mensajes. Lo peor es que la lista vive en tu cabeza, y crece.
Ahora imagina el otro arreglo. Abres un grupo de mensajería que se llama "Fiesta de Ana". Escribes una vez: "la fiesta se movió al sábado". Ya está. Tú no sabes exactamente quién está en el grupo —hay gente que agregó otra gente—, y no te importa: quien quiera enterarse se metió al grupo, y quien deje de interesarse se sale. Tu trabajo pasó de avisarle a cada quien a publicar el hecho.
Fíjate en lo que ganaste y en lo que perdiste, porque esa balanza es el módulo entero.
Ganaste que agregar a alguien nuevo ya no es problema tuyo. La tía que se enteró tarde entra al grupo sola. Tú no cambias nada, no te enteras siquiera. Antes, cada persona nueva significaba una línea más en tu lista mental; ahora significa cero trabajo para ti.
Perdiste que ya no sabes quién se enteró. Antes podías recorrer tu lista y decir con certeza: "estos cinco lo saben". Ahora publicaste al vacío. Si el del sonido no llega el sábado, tienes que ir a abrir el grupo, revisar quién está, y descubrir que nunca lo agregaron. Y perdiste el orden y la garantía: en la lista de mensajes tú sabías que primero le avisabas al salón y después al pastel; en el grupo, todos se enteran a la vez y cada quien reacciona cuando puede —o no reacciona.
Esa es exactamente la transacción que hace Observer. Cambias saber a quién le avisas por no tener que cambiar cuando cambian los interesados. Es un buen trato en muchos casos. Es un pésimo trato en otros. Y la mitad del módulo se va en enseñarte a distinguirlos.
Ejemplo trabajado: los avisos del checkout de Boletia, ocho meses después
En el módulo 1 leíste el checkout de Boletia. Su cuarta sección —la de los avisos— se veía así:
# Archivo: checkout/checkout.py (como estaba en el módulo 1)
# ---- 4. Avisos ---------------------------------------------------
customer = repository.get_customer(order.customer_id)
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))
organizer = repository.get_customer(event.organizer_id)
email_channel.send(organizer.email, build_organizer_alert(order, event))
analytics.track("order_paid", order_id=order.id, total=order.total)
return order
Siete líneas. Molesto, pero manejable. Ahora mira el mismo bloque hoy, ocho meses después. Nadie hizo nada malo; cada línea entró con una razón buena, revisada y aprobada:
# Archivo: checkout/checkout.py (hoy)
# ---- 4. Todo lo que pasa después de cobrar -------------------------
customer = repository.get_customer(order.customer_id)
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))
organizer = repository.get_customer(event.organizer_id)
email_channel.send(organizer.email, build_organizer_alert(order, event))
# Agregado en marzo por el equipo de operaciones: el inventario dejó de
# cuadrar porque nadie marcaba los boletos como vendidos.
for ticket in tickets:
ticket.status = "sold"
repository.save_ticket(ticket)
inventory.decrement_available(event.id, len(tickets))
# Agregado en mayo por contabilidad: factura automática arriba de cero.
if order.total > 0:
invoice = billing.create_invoice(order, customer)
email_channel.send(customer.email, build_invoice_email(invoice))
# Agregado en junio por el equipo de datos: el panel del organizador
# tenía que verse en vivo, no a la mañana siguiente.
analytics.track("order_paid", order_id=order.id, total=order.total)
reports.refresh_event_dashboard(event.id)
# Agregado hace dos semanas por marketing: programa de puntos.
loyalty.add_points(customer.id, points=int(order.total // 10))
return order
Veinte líneas donde había siete. Y esto es solo la sección 4 de una función que ya tenía tres secciones antes. Vamos a leerla despacio, porque el diagnóstico importa más que la solución.
Primer síntoma: la lista de importaciones del archivo. Para que ese bloque funcione, checkout.py tiene que importar email_channel, sms_channel, push_channel, repository, inventory, billing, analytics, reports y loyalty. Nueve módulos, y solo tres de ellos tienen que ver con cobrar. El archivo que se llama "checkout" conoce el módulo de facturación, el de reportes y el de puntos de lealtad. La función que orquesta la compra terminó conociendo medio sistema.
Segundo síntoma: la razón por la que este archivo cambia. Pregúntate qué tendría que pasar en Boletia para que alguien tenga que editar checkout.py. La respuesta debería ser algo como "que cambie la forma de cobrar". La respuesta real es: que cambie la forma de cobrar, o que marketing quiera puntos, o que contabilidad quiera facturas, o que datos quiera otro panel, o que operaciones cambie cómo se descuenta el inventario. Cinco razones para cambiar el archivo más delicado del sistema, y cuatro de ellas no tienen nada que ver con el checkout.
Tercer síntoma, el más caro: quién revisa esos cambios. Cada una de esas cuatro adiciones fue un cambio en checkout/checkout.py. Y checkout.py es el corazón: si se rompe, Boletia deja de vender. Así que cada vez que marketing quiere un ajuste en los puntos de lealtad, alguien tiene que revisar un cambio en el archivo donde nadie quiere equivocarse. El costo de coordinación es desproporcionado al tamaño del cambio.
Cuarto síntoma, el que más duele en producción. Fíjate en lo que pasa si billing.create_invoice() lanza una excepción. La compra ya se cobró —el dinero salió de la tarjeta del cliente— pero la función explota antes de llegar a analytics, a reports y a loyalty. Peor: explota después de haber mandado el correo de confirmación, así que el cliente tiene su boleto, el cobro está hecho, y el sistema cree que la compra falló. Una reacción secundaria puede tumbar la operación principal. Nadie diseñó eso; salió solo, de poner todo en la misma función y en el mismo try.
Qué esperar de esta lectura. Lo primero: no hay ningún villano. Cada línea la puso una persona razonable, resolviendo un problema real, por el camino más corto disponible. El camino más corto para "que pase X cuando se completa una compra" es, siempre, ir a la función de la compra y escribir X. Nadie va a inventar una arquitectura de eventos para agregar tres líneas. Esa es la razón por la que este problema aparece en todos lados: la degradación es el resultado natural de tomar la decisión localmente correcta muchas veces seguidas.
Lo segundo: fíjate en que el problema no es la cantidad de líneas. Si el checkout tuviera veinte líneas que todas tienen que ver con cobrar, no habría nada que discutir. El problema es que esas veinte líneas responden a cinco dueños distintos. Si te llevas una sola idea de esta lección, que sea esta: el olor no es "esta función es larga", es "esta función cambia por razones que no le pertenecen".
Y lo tercero, para que no salgas de aquí con el martillo listo: no todo ese bloque debería convertirse en evento. Marcar los boletos como vendidos, por ejemplo, no es una reacción opcional a la compra: es parte de la compra. Si eso falla, la compra debería fallar. Convertirlo en evento sería un error, y de los graves. Cuál sí y cuál no es literalmente el proyecto de la lección 8, y la lección 7 te da el criterio. Por ahora quédate con la sospecha.
El eje del módulo: quién reacciona
Cada familia de patrones se organiza alrededor de una pregunta. Vale la pena verlas juntas, porque así se entiende por qué esta guía está partida como está:
| Familia | La pregunta que ordena | Qué se separa |
|---|---|---|
| Módulo 3 — comportamiento que varía | ¿Qué cambia entre un caso y otro? | El algoritmo, de quien lo usa |
| Módulo 4 — crear objetos | ¿Cómo se construye esto? | La construcción, del uso |
| Módulo 5 — estructurar y adaptar | ¿Cómo hablan dos piezas que no se entienden? | La forma externa, de la interna |
| Módulo 6 — comunicar entre partes | ¿Quién reacciona cuando pasa algo? | Quien produce el hecho, de quien reacciona a él |
En los módulos 3, 4 y 5 el problema siempre tenía una forma parecida: hay un trabajo que hacer y varias maneras de hacerlo. Aquí el problema es distinto en su naturaleza. No hay varias maneras de "completar una compra". Hay una compra, y varias partes del sistema que quieren enterarse.
Ese cambio de eje trae un cambio de vocabulario que conviene instalar desde ahora, porque lo vas a usar en las siete lecciones siguientes:
- El hecho (o evento de dominio): algo que ya pasó y que no se puede deshacer. "Se completó la orden 4821". Fíjate en el tiempo verbal: pasado. Un hecho no pide nada, no ordena nada, solo informa. Esta distinción parece cosmética y no lo es; la lección 4 le dedica media sección.
- El publicador (o sujeto): la parte que produce el hecho y lo anuncia. En Boletia, el
checkout. - El suscriptor (u observador, o manejador): la parte que dijo "avísame cuando pase esto" y hace algo al respecto. En Boletia serían las notificaciones, el inventario, los reportes.
- La suscripción: el acto de registrarse como interesado. Es el corazón del asunto, porque es el único lugar donde el publicador y el suscriptor se tocan, y en muchos diseños ese lugar ni siquiera está en ninguno de los dos.
Una advertencia de nombres antes de seguir, porque en Boletia hay una colisión desafortunada. Event, en el modelo de datos de Boletia, significa un concierto o una conferencia — models/event.py, con su venue y su starts_at. Y "evento" en el vocabulario de este módulo significa un hecho que ocurrió en el sistema. Son dos cosas distintas con la misma palabra, y es exactamente el tipo de accidente que pasa en código real. Para que no te confundas, en esta guía vamos a hacer dos cosas: en la prosa vamos a decir "hecho" o "evento de dominio" cuando haya riesgo de ambigüedad, y en el código vamos a poner las clases de eventos en un paquete llamado bus/ en vez de events/. No es la solución más bonita del mundo; es la que evita que alguien lea from events import Event y no sepa si le están hablando de un concierto o de una compra.
Los dos patrones de la familia, en una línea cada uno
Este módulo cubre dos patrones. No son los únicos de la familia en el catálogo original —también están Mediator, Chain of Responsibility, Memento y algunos más— pero son los dos que de verdad vas a encontrar en código real y los dos que de verdad cambian cómo se ve un sistema.
Observer — avisar sin saber a quién. Quien produce el hecho lo publica; quien está interesado se suscribe. El publicador no conoce a los suscriptores, ni cuántos son, ni si hay alguno. Es el patrón central del módulo y ocupa las lecciones 2, 3 y 4. Vive detrás de casi todo lo que hoy se llama "arquitectura orientada a eventos", de los addEventListener del navegador, de las señales de Django y de los hooks de media industria.
Command — empaquetar una acción como objeto. En vez de llamar a una función, construyes un objeto que representa la llamada: qué hay que hacer y con qué datos. Como es un objeto, se puede guardar en una base de datos, meter en una cola, reintentar mañana, registrar en un historial o deshacer. Ocupa la lección 5, y esa lección tiene una advertencia grande y honesta: en la enorme mayoría del código, una función basta. Command se gana su lugar cuando la acción necesita sobrevivir al momento en que se decidió.
Los dos se combinan seguido y por eso están en el mismo módulo: un sistema maduro publica un hecho (Observer), y uno de los suscriptores, en vez de hacer el trabajo ahí mismo, empaqueta un Command y lo mete en una cola para que un trabajador lo ejecute con reintentos. Ese es, más o menos, el esqueleto de cualquier sistema de notificaciones serio. Boletia ya tiene media pieza de eso sin saberlo, y lo vamos a ver.
Lo que esta familia cobra
Aquí es donde este módulo se separa de la mayoría de las explicaciones que vas a encontrar afuera. Voy a adelantarte el costo antes de enseñarte el patrón, a propósito, porque el orden importa: si ves primero lo elegante que queda el checkout con eventos, después ya no vas a poder mirar el costo con los ojos limpios.
El costo principal: el flujo deja de leerse. Hoy, si alguien te pregunta "¿qué pasa cuando se completa una compra en Boletia?", la respuesta está en un lugar: abres checkout.py, lees hasta abajo, y ya lo sabes todo. Es feo, sí. Pero es completo y local. Con eventos, esa misma pregunta se vuelve una búsqueda: hay que encontrar todos los suscriptores de OrderCompleted, que están repartidos en cinco archivos de cinco carpetas distintas, y que además pueden registrarse de forma dinámica según la configuración. La función del checkout terminará diciendo bus.publish(OrderCompleted(...)) y ahí se acaba lo que puedes leer.
Ese costo no se paga cuando escribes el código. Se paga cuando alguien —tal vez tú, en un año— tiene que entenderlo bajo presión. La lección 6 se llama "el flujo que ya no puedes seguir con el dedo" y no es una metáfora bonita: es literalmente la habilidad que pierdes, la de poner el dedo en la pantalla y seguir la ejecución línea por línea.
El segundo costo: el orden y las garantías se vuelven borrosos. En el bloque de veinte líneas de arriba hay algo valioso que no salta a la vista: sabes exactamente en qué orden pasa todo. Primero el correo al cliente, después el aviso al organizador, después el inventario. Cuando publicas un evento y tres suscriptores reaccionan, el orden depende de en qué orden se registraron —un detalle que vive en otro archivo— y confiar en ese orden es una de las formas más sutiles de escribir un bug que solo aparece en producción.
El tercer costo: los errores se esconden. Si un suscriptor lanza una excepción, ¿qué pasa con los otros? ¿Se detiene todo? ¿Se ignora y siguen los demás? Las dos respuestas son defendibles y las dos tienen consecuencias feas. En el código directo, esa pregunta no existe: la excepción sube y ya. Con eventos hay que decidirlo, documentarlo y que todo el equipo lo sepa.
El cuarto costo: la tentación de publicar todo. Este es de criterio y es el que más veces he visto arruinar un sistema. Una vez que el bus existe, publicar un evento es gratis y se siente profesional. A los seis meses hay cuarenta tipos de evento, la mitad con un solo suscriptor, y nadie es capaz de dibujar el flujo del sistema en una pizarra. Un bus de eventos con cuarenta tipos y sesenta suscripciones no es una arquitectura desacoplada: es un goto distribuido con mejor prensa.
Ninguno de estos cuatro costos es un argumento para no usar Observer. Son el precio de una compra que muchas veces vale la pena. Pero son el precio, y hay que verlo escrito antes de firmar.
El criterio del módulo 2 sigue encendido
Vale la pena repetirlo, porque este módulo es donde más fácil se apaga. La regla de tres que viste en el módulo 2 aplica aquí con una traducción directa: el evento se gana su lugar cuando aparece el tercer interesado, no cuando aparece el primero.
Con un solo interesado, publicar un evento es pura ceremonia: escribes una clase de evento, un bus, un registro de suscripción y un manejador, para lograr exactamente lo mismo que una línea de llamada directa. Con dos interesados fijos que no van a cambiar, la llamada directa sigue siendo más clara y —esto importa— más fácil de borrar el día que sobre. Con tres o más, especialmente si vienen de módulos distintos y siguen llegando, el evento empieza a pagar.
Hay una asimetría que conviene tener presente desde ahora y que la lección 7 desarrolla. Pasar de llamada directa a evento es un refactor barato y mecánico: extraes las llamadas, defines el hecho, registras los manejadores. Pasar de evento a llamada directa es caro, porque para hacerlo tienes que descubrir primero quién estaba escuchando, y esa información ya no está en el código: está repartida. La dirección barata es la que va del código simple al código con eventos, no al revés. Eso es un argumento fuerte para empezar simple, no una excusa para no empezar nunca.
Errores comunes
Convertir en evento todo lo que pasa después de una operación (de criterio). Qué pasa: alguien aprende Observer, mira las veinte líneas del checkout y publica un evento por cada una. El inventario, la factura, los puntos, los reportes y las notificaciones se vuelven suscriptores de OrderCompleted. El checkout queda hermoso —tres líneas— y al mes siguiente empiezan los reportes de inventario descuadrado. Por qué pasa: porque el bloque parece homogéneo. Todas son cosas que pasan después de cobrar, así que se sienten del mismo tipo. Pero no lo son: marcar los boletos como vendidos es parte de la transacción de compra, y mandar el correo no. Cómo detectarlo: hazte esta pregunta por cada línea — "si esto falla, ¿la compra debería considerarse fallida?". Si la respuesta es sí, no es una reacción: es un paso de la operación, y no debe salir de ahí. Cómo corregirlo: la lección 7 completa, y en el proyecto vas a tener que justificar esa decisión línea por línea.
Creer que con eventos el acoplamiento desaparece (conceptual). Qué pasa: se presenta Observer como "ahora el checkout no depende de nadie" y se da por resuelto el problema del acoplamiento. Meses después alguien agrega un campo al evento, o le cambia el nombre a uno, y se rompen cuatro suscriptores en cuatro módulos distintos —sin que el compilador ni las pruebas del checkout digan nada—. Por qué pasa: porque el acoplamiento se volvió invisible, y lo invisible se confunde fácilmente con lo inexistente. La dependencia no se fue: se mudó del nombre de la función a la forma del evento, que es un contrato como cualquier otro, con la desventaja de que ningún lenguaje lo verifica por ti. Cómo detectarlo: pregunta "si cambio este evento, ¿quién se rompe?". Si no sabes contestar sin buscar en todo el repositorio, tienes acoplamiento y además no lo puedes ver. Cómo corregirlo: la lección 4 entera, que trata sobre cómo se diseña un evento que aguante.
Medir el éxito por lo corto que quedó el checkout (de criterio). Qué pasa: se juzga el refactor por el archivo que se limpió, no por el sistema completo. El checkout pasó de trescientas líneas a ciento veinte y todos aplauden; nadie cuenta que aparecieron seis archivos nuevos, un bus, un módulo de registro y una capa de indirección que todo el equipo tiene que aprender. Por qué pasa: porque la mejora es visible y concentrada, y el costo es invisible y repartido. Es el mismo sesgo del módulo 2, con otro traje. Cómo detectarlo: si tu justificación del cambio cabe en la frase "quedó más limpio", no tienes una justificación, tienes una impresión. Cómo corregirlo: obliga a que la justificación mencione un cambio futuro concreto que se vuelve más barato. "Cuando entre el sexto interesado, no habrá que tocar el checkout ni pedirle revisión al equipo de pagos" es una justificación. "Quedó más limpio" no lo es.
Ejercicios
Ejercicio 1 — Clasifica las reacciones del checkout. Vuelve al bloque de veinte líneas de hoy. Para cada una de estas siete reacciones, decide si es parte de la compra (si falla, la compra falló) o una reacción a la compra (si falla, la compra sigue siendo válida): (a) mandar el correo de confirmación al cliente, (b) marcar los boletos como sold, (c) descontar el inventario disponible, (d) avisarle al organizador, (e) crear la factura, (f) registrar el evento en analítica, (g) sumar puntos de lealtad. Justifica cada una en una línea, desde el punto de vista del cliente que compró.
Ver solución
La clave del ejercicio es que la respuesta no sale del código: sale del negocio. La pregunta que decide es "¿el cliente compró o no compró?".
(b) y (c) son parte de la compra. Si los boletos no quedaron marcados como vendidos, el sistema los va a vender otra vez y dos personas van a llegar al mismo asiento. Esto no es una reacción: es el otro lado de la transacción. Si falla, la compra tiene que fallar y hay que devolver el dinero.
(e), la factura, es discutible y depende del país. En muchos lugares la factura es un requisito legal de la venta, pero se puede emitir minutos después sin que la venta sea inválida. Yo la trataría como reacción, con la condición de que exista un mecanismo de reintento fiable —si se pierde, hay un problema legal, no solo un correo que no llegó—. Que sea discutible es el punto: en el proyecto vas a tener que argumentar tu decisión, no adivinar la mía.
(a), (d), (f) y (g) son reacciones. Si el correo no llega, el cliente compró igual y tiene su boleto en la aplicación. Si el organizador no se entera al instante, se entera en su panel. Si analítica pierde un registro, el panel muestra un número un poco menor. Si los puntos no se suman, se pueden recalcular. Ninguna de esas fallas debería tumbar una venta que ya se cobró.
Por qué funciona: acabas de hacer, a mano y sin patrones, el 80% del trabajo de decisión del módulo. La estructura —bus, eventos, suscriptores— es la parte fácil y mecánica. Decidir qué entra y qué no es la parte que requiere criterio, y es la que se evalúa en el proyecto.
Ejercicio 2 — Encuentra tu propio bloque. Piensa en un sistema que conozcas: uno donde hayas trabajado, uno de un proyecto personal, o incluso un proceso de tu trabajo que no sea software. Encuentra una operación que dispare varias reacciones. Escribe: (a) la operación y las reacciones que dispara, (b) cuántas de esas reacciones se agregaron después de que la operación ya existía, y (c) quién tuvo que tocar el código de la operación cada vez.
Ver solución
No hay respuesta única, pero el patrón que casi siempre aparece sí lo es, y vale la pena que lo veas escrito.
Sobre (a): los candidatos típicos son "se registró un usuario" (correo de bienvenida, alta en el CRM, evento de analítica, notificación al equipo de ventas), "se subió un archivo" (miniatura, indexado para búsqueda, antivirus, aviso al dueño de la carpeta) y "se cerró un ticket" (encuesta de satisfacción, actualización de métricas, aviso al reportante). Si tu ejemplo no es software —el alta de un empleado, por ejemplo— vas a encontrar exactamente la misma forma: recursos humanos, sistemas, seguridad y nómina, todos reaccionando al mismo hecho.
Sobre (b): la respuesta casi siempre es "la mayoría". La operación nació con una o dos reacciones y las otras se acumularon. Esa acumulación es la señal más fuerte de que un evento se gana su lugar: el número de interesados creció con el tiempo y no hay razón para creer que dejó de crecer.
Sobre (c): aquí está el hallazgo. Normalmente quien tuvo que tocar el código de la operación no era el dueño de la reacción. Alguien de marketing pidió el correo y un desarrollador del equipo de la operación lo escribió. Ese desajuste —el dueño del interés y el dueño del código no son la misma persona— es el argumento más fuerte a favor del evento, y es distinto del argumento técnico. Es un argumento organizacional, y suele pesar más.
Por qué funciona: te saca del ejemplo de la guía y te muestra que este problema no es de Boletia ni de Python. Es una forma que aparece en cualquier sistema donde varias partes tengan interés en el mismo hecho, incluidos los sistemas que no tienen código.
Ejercicio 3 — El escenario del fallo. Vuelve al bloque de hoy y supón que billing.create_invoice(order, customer) lanza una excepción porque el servicio de facturación está caído. Escribe qué pasa exactamente, en orden, desde el punto de vista de: (a) el dinero del cliente, (b) el correo que recibió el cliente, (c) el inventario, (d) los puntos de lealtad, (e) la respuesta HTTP que ve la aplicación móvil. Después, escribe en una frase por qué ese conjunto de resultados es peor que cualquiera de sus partes.
Ver solución
Vamos en orden de ejecución, porque el orden es justo lo que produce el desastre.
(a) El dinero ya salió. El cobro ocurre en la sección 3, mucho antes. La tarjeta del cliente tiene el cargo.
(b) El cliente ya recibió su correo de confirmación, con su boleto. Ese correo se manda al principio de la sección 4.
(c) El inventario ya se descontó y los boletos ya están marcados como sold. También ocurre antes de la facturación.
(d) Los puntos no se sumaron, porque esa línea está después de la que explotó. Tampoco se registró nada en analítica ni se refrescó el panel del organizador.
(e) La excepción sube por la función, sale de checkout(), llega a api/routes.py y —a menos que alguien la haya atrapado— la aplicación móvil recibe un 500. Desde la aplicación, la compra falló.
Por qué el conjunto es peor que las partes: porque el sistema quedó en un estado que no corresponde a ninguna realidad. No es "la compra se hizo" ni "la compra no se hizo": es una compra a medias, con el dinero cobrado, el boleto entregado, el inventario descontado y la aplicación diciéndole al cliente que reintente. Y si el cliente reintenta —cosa que va a hacer— compra dos veces. Un solo servicio caído, que además es el menos importante de los cinco, produjo un cobro duplicado.
Ese es el argumento técnico más fuerte del módulo, y fíjate en que no menciona ningún patrón. No se trata de elegancia: se trata de que una reacción secundaria no debería poder tumbar una operación principal, y hoy puede. Cualquier solución que arregle eso —eventos, colas, un simple try/except alrededor de cada reacción— es mejor que lo que hay. Que la mejor sea un evento es una pregunta aparte, y la contestamos en la lección 7.
Resumen y siguiente paso
En esta lección viste el problema del módulo con nombre y apellido: el checkout de Boletia pasó de siete líneas de avisos a veinte en ocho meses, porque cada equipo que quiso enterarse de una compra fue a escribir su llamada ahí. Diagnosticaste los cuatro síntomas —el archivo conoce medio sistema, cambia por cinco razones ajenas, obliga a revisar el corazón del producto por cambios triviales, y deja que una reacción secundaria tumbe la operación principal— y viste que ninguno de ellos vino de una mala decisión, sino de muchas decisiones localmente correctas.
Instalaste el eje del módulo, que no es qué varía sino quién reacciona, con su vocabulario: el hecho, el publicador, el suscriptor y la suscripción. Conociste los dos patrones que vienen —Observer y Command— y, sobre todo, viste el precio antes de ver el producto: el flujo que deja de leerse, el orden que se vuelve borroso, los errores que se esconden y la tentación de publicar todo.
Antes de avanzar deberías poder: explicar en una frase por qué el checkout de Boletia cambia por razones que no le pertenecen; distinguir una reacción de un paso de la operación con la pregunta "si esto falla, ¿la compra falló?"; y nombrar al menos dos de los cuatro costos de esta familia sin volver a leer.
Lo que todavía no tienes es el mecanismo. Sabes qué problema hay y sabes que se llama Observer, pero no has visto sus piezas ni —lo más importante— entendido por qué la palabra clave de todo esto es inversión: quién depende de quién antes y después. La lección 2 desarma el patrón pieza por pieza, y te va a dejar viendo esa inversión con claridad, porque es el motivo real por el que el patrón funciona y el motivo real por el que cuesta.
Recursos
- Refactoring Guru — Observer — la explicación visual del patrón, con ejemplos en varios lenguajes. Útil como referencia de la forma; el criterio de cuándo usarlo lo damos aquí.
- Martin Fowler — What do you mean by "Event-Driven"? — el artículo que ordena la confusión de términos alrededor de los eventos. Fowler distingue cuatro cosas distintas que la gente llama "event-driven"; leerlo evita media discusión de equipo.
- Martin Fowler — Domain Event — la definición corta de "hecho que ocurrió en el dominio", que es exactamente el vocabulario que usamos en este módulo.
- The Grug Brained Developer — la sección sobre complejidad aplica a este módulo mejor que a ningún otro: los eventos son la forma más elegante de esconder complejidad sin eliminarla.