Módulo 6: Patrones para comunicar entre partes

7. Cuándo una llamada directa es más honesta

Descripción

Al terminar esta lección vas a tener la vacuna del módulo. Después de cinco lecciones construyendo un sistema de eventos, esta se dedica a lo contrario: los casos —muchos, y más frecuentes de lo que sugiere la literatura— donde llamar directo es la mejor decisión de ingeniería disponible.

Vas a salir con cuatro cosas concretas. Primero, doce señales: seis que indican que el evento se gana su lugar y seis que indican que no. No son opiniones: cada una se puede verificar mirando el código y el historial del repositorio. Segundo, el punto medio que casi nadie considera, que se lleva el 80% del beneficio con el 5% del costo y que en mi experiencia es la respuesta correcta más veces que cualquiera de los dos extremos. Tercero, la asimetría del costo de cambiar de opinión, que es el argumento más fuerte a favor de empezar simple y el que casi nunca se dice en voz alta. Y cuarto, la traducción de la regla de tres del módulo 2 al vocabulario de este módulo.

Esta lección existe porque el módulo, tal como va, es peligroso. Acabas de ver un patrón elegante aplicado a un caso donde funciona, con su costo bien medido y sus mitigaciones. Es exactamente la configuración mental que produce el síndrome del martillo nuevo del que hablaba el módulo 1: durante las próximas semanas vas a ver eventos en todos lados, y algunos de esos lugares no los tienen. Vamos a arreglar eso ahora, mientras el material está fresco.

Conexión con el módulo: la lección 6 midió el costo y te dio las mitigaciones. Esta lección usa ese costo como el argumento central para no pagarlo cuando no hace falta. Es la aplicación directa del módulo 2 —cada abstracción se paga, la regla de tres, YAGNI— sobre esta familia concreta. Y es la preparación inmediata del proyecto: en la lección 8 vas a tener que decidir, reacción por reacción, cuál merece ser evento y cuál se queda, y estas doce señales son la herramienta con la que vas a defender cada decisión.

Un grupo de mensajería para dos personas

Vuelve a la fiesta sorpresa de la lección 1. Ahí el grupo de mensajería ganaba claramente: doce personas, la lista cambiaba, tú eras el cuello de botella.

Ahora cambia el escenario. Son dos personas: tú y tu hermano. Van a comprar un regalo entre los dos.

¿Abres un grupo? Nadie abre un grupo para dos personas. Le escribes directo. Y no es pereza: es que el grupo, con dos personas, es peor. Tienes que crearlo, ponerle nombre, agregar a alguien que ya tenías en tus contactos, y el resultado es una conversación exactamente igual que la que ya podías tener, con un paso más y una notificación distinta. Cuando termine el asunto del regalo, además, va a quedar ahí un grupo muerto que ninguno de los dos se atreve a borrar.

Fíjate en la última parte, porque es la que menos se piensa: la conversación directa se termina sola. El grupo hay que desmantelarlo. En código pasa exactamente igual, y lo vamos a ver medido: una llamada directa que sobra se borra en una línea; un evento que sobra hay que rastrearlo, encontrar a sus suscriptores y desmontarlo pieza por pieza.

Y ahora el matiz que hace interesante la analogía. ¿Cuándo sí abrirías el grupo con dos personas? Si sabes que van a entrar más. Si el asunto va a durar meses. Si quieres que la información quede en un lugar al que otros puedan sumarse. Esas condiciones son verificables: o las tienes o no las tienes. No abres el grupo "por si acaso"; lo abres cuando sabes algo concreto sobre el futuro.

Ahí está el criterio completo. Vamos a bajarlo a código.

Ejemplo trabajado: el evento que quitamos

Boletia tiene un evento que sobra, y vale la pena verlo porque quitar es la operación que nunca se enseña.

Hace cuatro meses, con el entusiasmo del bus recién estrenado, alguien implementó la transferencia de boletos —un asistente le pasa su boleto a otra persona— y publicó un evento:

# Archivo: bus/events.py
@dataclass(frozen=True)
class TicketTransferred:
    ticket_id: int
    from_customer_id: int
    to_customer_id: int
    occurred_at: str


# Archivo: bus/wiring.py
    bus.subscribe(TicketTransferred, send_transfer_confirmation)


# Archivo: notifications/subscribers.py
def send_transfer_confirmation(transferred: TicketTransferred) -> None:
    """Avisa a las dos partes que la transferencia se completó."""
    sender = repository.get_customer(transferred.from_customer_id)
    receiver = repository.get_customer(transferred.to_customer_id)
    ticket = repository.get_ticket(transferred.ticket_id)
    notify(sender, build_transfer_sent(ticket, receiver))
    notify(receiver, build_transfer_received(ticket, sender))


# Archivo: tickets/transfer.py
def transfer_ticket(ticket_id, to_customer_id):
    ticket = repository.get_ticket(ticket_id)
    if ticket.kind == "courtesy":
        raise NotTransferable("Las cortesías no se transfieren")
    previous_owner_id = ticket.customer_id
    ticket.customer_id = to_customer_id
    repository.save_ticket(ticket)
    bus.publish(TicketTransferred(
        ticket_id=ticket.id,
        from_customer_id=previous_owner_id,
        to_customer_id=to_customer_id,
        occurred_at=now(),
    ))

Cuatro archivos. Se ve profesional, es consistente con el resto del sistema y nadie lo cuestionó en la revisión, precisamente porque era consistente.

Cuatro meses después, los hechos: hay un suscriptor. Nunca hubo un segundo. Nadie pidió reaccionar a una transferencia. Y en el registro de describe() aparece un evento con un solo manejador, que es exactamente la señal que el acuerdo de la lección 6 declaraba inaceptable.

Vamos a quitarlo. Así queda:

# Archivo: tickets/transfer.py
from notifications.transfers import notify_transfer_completed


def transfer_ticket(ticket_id, to_customer_id):
    ticket = repository.get_ticket(ticket_id)
    if ticket.kind == "courtesy":
        raise NotTransferable("Las cortesías no se transfieren")
    previous_owner_id = ticket.customer_id
    ticket.customer_id = to_customer_id
    repository.save_ticket(ticket)
    # Una llamada, con nombre, a la única cosa que reacciona.
    notify_transfer_completed(ticket, previous_owner_id, to_customer_id)
# Archivo: notifications/transfers.py
def notify_transfer_completed(ticket, from_customer_id, to_customer_id) -> None:
    """Avisa a las dos partes que la transferencia se completó."""
    sender = repository.get_customer(from_customer_id)
    receiver = repository.get_customer(to_customer_id)
    notify(sender, build_transfer_sent(ticket, receiver))
    notify(receiver, build_transfer_received(ticket, sender))

Qué se ganó, contado. De cuatro archivos a dos. Desapareció un tipo de evento del contrato compartido, del describe() y del mapa —el sistema tiene un concepto menos que aprender—. La función transfer_ticket() ahora dice qué hace: leyéndola sabes que se avisa a las dos partes, y eso antes exigía tres saltos. Y el manejador se ahorró dos consultas: transfer_ticket ya tenía el ticket en la mano, así que el evento lo estaba obligando a buscarlo otra vez por identificador, que es un costo tonto que el evento cobraba solo por existir.

Qué se perdió. Que el día que aparezca un segundo interesado —digamos, actualizar la lista de asistentes que el organizador descarga— habrá que tocar tickets/transfer.py para agregar la segunda llamada. Un archivo, dos líneas. Ese es todo el costo real de haber quitado el evento.

Qué costó quitarlo. Y aquí está la lección escondida. Agregarlo fue escribir cuatro archivos nuevos, media hora, sin riesgo. Quitarlo exigió primero averiguar quién escuchaba, y esa pregunta no se contesta leyendo transfer.py. Hubo que ir al describe(), confirmar que solo había un suscriptor, buscar por si alguien se suscribía en otro lado, y recién entonces tocar código. En un sistema sin las mitigaciones de la lección 6, ese paso solo puede tomar horas y termina en una decisión con dudas.

Qué esperar de este ejemplo. Lo primero: fíjate en que el código quitado no estaba mal escrito. Estaba bien escrito, era consistente y hacía exactamente lo que decía. Sobraba, que es otra cosa. Reconocer código correcto que sobra es una habilidad distinta —y más difícil— que reconocer código incorrecto, y es la habilidad central del módulo 2.

Lo segundo: nota que la alternativa no fue "meter las notificaciones dentro de transfer_ticket". Fue una llamada a una función con nombre en su propio módulo. La reacción sigue viviendo en notifications/, sigue teniendo nombre propio y sigue siendo fácil de probar por separado. Perdimos la inversión de la dependencia —transfer.py ahora importa notifications— y conservamos todo lo demás. Ese punto medio es el tema de la sección de más abajo.

Y lo tercero, para tu criterio: la persona que puso el evento hizo lo mismo que había funcionado la vez anterior. Eso no es un error de juicio, es cómo trabaja cualquier equipo sano. El error, si es que hay uno, es del sistema: no había ningún momento programado para preguntarse si la decisión seguía valiendo. Por eso el acuerdo de la lección 6 incluía una revisión trimestral. Los patrones que sobran casi nunca entran por una mala decisión; entran por la falta de una revisión.

Las seis señales de que el evento sí conviene

Todas son verificables. Si no puedes verificarla mirando el código o el historial, no cuenta como señal.

Señal 1 — Ya hay tres o más interesados. La regla de tres del módulo 2, traducida. Con tres, el costo de coordinación de las llamadas directas ya se siente y el mapa de suscripciones ya se paga. Cómo verificarla: cuenta las reacciones que existen hoy, no las que imaginas.

Señal 2 — Los interesados aparecen seguido. No es lo mismo tres estables que tres que llegaron este año. Cómo verificarla: mira el historial del archivo. git log sobre checkout.py te dice cuántas veces se tocó por razones ajenas al checkout. Si son tres o cuatro veces al año, el evento paga; si el archivo lleva dos años sin que nadie agregue una reacción, no.

Señal 3 — Los interesados vienen de módulos o equipos distintos. Este argumento es organizacional y suele pesar más que el técnico. Si notificaciones, inventario y facturación las mantienen tres personas distintas, cada llamada directa es una coordinación. Cómo verificarla: mira quién escribe habitualmente cada carpeta. Si es la misma persona, la señal no aplica.

Señal 4 — Las reacciones no deben bloquear la operación principal. Si las reacciones son lentas o pueden fallar por causas externas y aun así la operación es válida, tenerlas colgadas de la función principal es un riesgo. Cómo verificarla: la pregunta de la lección 1 —"si esto falla, ¿la compra falló?"—. Si la respuesta es no para varias reacciones, la señal está.

Señal 5 — El publicador no debería conocer a los interesados, por diseño. Hay casos donde el desconocimiento es un requisito, no una comodidad: si terceros pueden escribir extensiones, el código del núcleo no puede conocerlas porque no existen cuando se escribe. Cómo verificarla: ¿alguien fuera de tu repositorio necesita reaccionar? Si sí, la señal es fuerte y casi decisiva.

Señal 6 — Los interesados tienen que poder desaparecer. En algunos sistemas los suscriptores van y vienen con el ciclo de vida: pantallas que se abren y se cierran, sesiones, conexiones. Ahí Observer no es una comodidad, es el mecanismo natural. Cómo verificarla: ¿el conjunto de interesados cambia mientras el programa corre? Si sí, la señal está. En Boletia, no lo hace: el cableado se arma al arrancar y no se mueve.

Las seis señales de que la llamada directa es mejor

Señal A — Hay uno o dos interesados y son estables. Uno solo es decisivo por sí mismo: un evento con un suscriptor es una llamada directa con tres archivos de por medio. Con dos estables, la llamada sigue ganando.

Señal B — Necesitas el resultado. Si quien produce el hecho necesita saber qué pasó —un valor de vuelta, una confirmación, un error que decida el siguiente paso— no tienes una notificación, tienes una llamada. Un publish que devuelve algo útil es la señal más clara de que el patrón está mal aplicado.

Señal C — El orden importa de verdad. Si la reacción B debe correr después de la A porque depende de su efecto, no son dos reacciones independientes: es un procedimiento con pasos. Se escribe como función. Confiar en el orden de suscripción es apoyar una regla de negocio en un detalle que nadie protege.

Señal D — Debe ocurrir en la misma transacción. Es el caso del inventario de la lección 3. Si la reacción y la operación tienen que tener éxito o fallar juntas, el evento introduce una grieta por la que se cuelan estados imposibles. Esta señal es decisiva: gana sobre cualquiera de las seis anteriores.

Señal E — Los dos lados son de la misma persona y del mismo módulo. El beneficio principal del evento es de coordinación entre partes distintas. Si quien publica y quien escucha son el mismo módulo, mantenido por la misma persona, no hay coordinación que ahorrar y solo queda el costo.

Señal F — Es código que probablemente se borre. Un experimento, una prueba con usuarios, una campaña de tres meses, algo detrás de un interruptor de configuración. Aquí la llamada directa gana por una razón que rara vez se dice: es más fácil de borrar. Una línea que se quita frente a cuatro archivos que hay que rastrear. Para código de vida corta, la facilidad de borrado importa más que la facilidad de extensión.

Fíjate en que la señal F apunta a la misma propiedad que la analogía del grupo de mensajería: la conversación directa se termina sola, el grupo hay que desmantelarlo. Y fíjate en la señal D: es la única de las doce que es decisiva por sí sola. Todas las demás se pesan; esa manda.

El punto medio que casi nadie considera

Casi todas las discusiones sobre esto se plantean como si hubiera dos opciones: veinte líneas metidas en el checkout o un bus de eventos completo. Hay una tercera, y es la que yo elegiría en la mayoría de los sistemas del tamaño de Boletia.

Extraer las reacciones a una función con nombre, y llamarla directo.

# Archivo: checkout/after_purchase.py
"""Todo lo que Boletia hace después de que una compra se confirma.

Este archivo existe para que checkout.py no tenga que conocerlo. Si
mañana hay que agregar una reacción, se agrega AQUÍ, y el checkout no
se toca ni necesita revisión del equipo que lo cuida.
"""
from notifications.notifier import notify
from notifications.templates import build_confirmation, build_organizer_alert


def run_after_purchase(order, customer, event, tickets) -> None:
    """Ejecuta las reacciones a una compra confirmada.

    El orden es deliberado y está documentado: primero lo que el cliente
    espera ver, después lo interno. Cada reacción va aislada, porque
    ninguna de ellas debe poder tumbar una venta ya cobrada.
    """
    _safely(lambda: notify(customer, build_confirmation(order)), "confirmación")
    _safely(lambda: _alert_organizer(order, event), "aviso al organizador")
    _safely(lambda: billing.issue(order, customer), "factura")
    _safely(lambda: analytics.track_paid(order), "analítica")
    _safely(lambda: loyalty.add_for(order), "puntos")


def _safely(action, label: str) -> None:
    """Corre una reacción sin dejar que su falla tumbe la compra.

    Es lo mismo que hacía el bus, sin el bus. Y con una ventaja: aquí se
    lee en dos líneas qué política de errores tiene el sistema, en vez de
    tener que abrirlo en otro archivo.
    """
    try:
        action()
    except Exception:
        logger.exception("Falló la reacción posterior a la compra: %s", label)
# Archivo: checkout/checkout.py
    # ---- 4. Reacciones a la compra -------------------------------------
    run_after_purchase(order, customer, event, tickets)
    return order

Mira lo que este arreglo consigue y lo que no.

Consigue que checkout.py deje de importar nueve módulos: importa uno. Consigue que las reacciones tengan un hogar con nombre, donde agregar la sexta no toca el checkout ni requiere la revisión de quien lo cuida. Consigue el aislamiento de errores, con la política de fallas escrita en dos líneas legibles. Y consigue —esto es lo grande— que la respuesta a "¿qué pasa cuando se completa una compra?" siga siendo abrir un archivo y leerlo de arriba abajo. El dedo sobre la pantalla sigue funcionando. El editor sigue encontrando usos. La pila de llamadas sigue completa. No hay concepto nuevo que aprender.

No consigue la inversión de la dependencia: after_purchase.py conoce a los cinco módulos, así que agregar un interesado sigue tocando un archivo que ya existe. Y no consigue que equipos distintos trabajen sin pisarse: si notificaciones y facturación son de dos equipos, los dos editan el mismo archivo.

Ahora la pregunta que importa: de los cuatro síntomas que diagnosticamos en la lección 1, este arreglo resuelve tres. El archivo ya no conoce medio sistema. Ya no cambia por cinco razones ajenas. Ya no deja que una reacción secundaria tumbe la venta. El cuarto —que agregar un interesado obligue a tocar un archivo compartido— sigue ahí, pero el archivo que se toca pasó de ser checkout.py, el corazón del producto, a ser after_purchase.py, un archivo cuyo peor caso es que los correos salgan mal.

Ese es un arreglo de veinte minutos, sin conceptos nuevos, sin costo de aprendizaje, sin mapa que mantener y sin pérdida de trazabilidad. Compáralo con el sistema completo de las lecciones 3 a 6 —bus, eventos, cableado, describe(), trazas, pruebas de contrato, acuerdo de equipo— y pregúntate honestamente cuál te conviene más si tu equipo tiene seis personas.

Yo, Mike, te lo digo sin rodeos: en la mayoría de los sistemas que he visto, este punto medio era la respuesta correcta y nadie lo consideró, porque la discusión estaba planteada entre el código feo y el patrón elegante. La opción intermedia no tiene nombre en el catálogo, no luce en una entrevista y no genera conversación en internet. Y funciona.

Cuándo sí conviene ir al bus completo: cuando aparece la señal 3 con fuerza —equipos distintos que se pisan en ese archivo— o la señal 5 —terceros que tienen que poder engancharse—. Ahí la inversión de la dependencia deja de ser elegancia y pasa a ser lo que estás comprando.

La asimetría del costo de cambiar de opinión

Este es el argumento que más peso tiene y el que menos se dice.

De llamada directa a evento: barato y mecánico. Tienes las reacciones a la vista, en una función, en orden. Defines el hecho, mueves cada bloque a su manejador, registras las suscripciones, reemplazas la función por un publish. Es un refactor de una tarde, con toda la información disponible desde el principio, y las pruebas existentes te cubren.

De evento a llamada directa: caro e incierto. El primer paso —averiguar quién escucha— no lo puedes hacer leyendo el código del publicador, porque esa información no está ahí. Con las mitigaciones de la lección 6, es un describe() y quince minutos. Sin ellas, es una búsqueda por todo el repositorio y una conclusión con dudas: siempre puede quedar un suscriptor que se registra desde un módulo que solo se importa con cierta configuración.

Esa asimetría tiene una consecuencia directa: en la duda, empieza con la llamada directa. No porque sea mejor en abstracto, sino porque el camino que probablemente vas a recorrer —simple primero, evento después si hace falta— es el barato, y el camino contrario es el caro. Es exactamente el razonamiento de YAGNI del módulo 2, con una justificación mecánica en vez de moral: no es "no lo vas a necesitar", es "si resulta que lo necesitas, agregarlo va a costar poco; si resulta que no, quitarlo va a costar mucho".

Y viene con una obligación, para que no se convierta en excusa: si dejas la llamada directa, tienes que dejarla fácil de convertir. Reacciones agrupadas en una función con nombre, cada una en una línea, sin lógica de negocio entremezclada. Eso es exactamente el punto medio de la sección anterior, y por eso es tan buena posición de partida: es la forma que menos cuesta convertir en evento el día que las señales aparezcan.

La regla de tres, traducida a eventos

Del módulo 2 te llevaste la regla de tres: no abstraigas con dos casos, espera al tercero. La traducción a esta familia es directa, con dos matices propios.

La regla: el evento se gana su lugar con el tercer interesado, no con el primero ni con el segundo.

Matiz uno: cuenta interesados, no líneas. Cinco líneas de notificación al mismo destinatario son un interesado. La pregunta no es cuánto código hay después de la operación, es cuántas partes del sistema tienen un motivo propio para reaccionar. Es el mismo matiz que la lección 3 del módulo 2 hacía con las abstracciones: se cuenta conocimiento, no texto.

Matiz dos: el tercero tiene que ser real. "Vamos a necesitar avisarle a contabilidad el próximo trimestre" no es un tercer interesado: es una intención. Los interesados imaginarios son la principal fuente de eventos que sobran, porque a diferencia de una abstracción prematura —que al menos molesta a quien lee— un evento con un solo suscriptor no molesta a nadie y por eso nadie lo quita.

Y un tercer criterio que en esta familia funciona mejor que el conteo, y que yo usaría como pregunta principal: ¿cuántas veces en el último año se tocó este archivo por una razón que no le pertenece? Es una pregunta que el historial del repositorio contesta con datos, no con impresiones. Tres o más veces al año, el evento paga. Cero veces en dos años, el evento es puro costo por más interesados que haya, porque nadie está pagando el precio que el evento evita.

Errores comunes

Poner el evento "para no tener que decidirlo después" (de criterio). Qué pasa: hay un interesado, pero alguien argumenta que es mejor dejarlo preparado, y se escribe el bus completo. El segundo interesado nunca llega, y el sistema carga durante años cuatro archivos y un concepto extra por una predicción que no se cumplió. Por qué pasa: porque el costo de agregar el evento se paga hoy, es visible y es chico; el costo de mantenerlo se paga durante años, es invisible y lo pagan otros. Cómo detectarlo: si tu justificación usa un tiempo verbal futuro —"vamos a necesitar", "cuando llegue"— y no puedes nombrar al segundo interesado con nombre y apellido, es una predicción. Cómo corregirlo: la asimetría. Recuerda que agregar el evento después cuesta una tarde y quitarlo cuesta más. En la duda, la llamada directa, agrupada en una función con nombre para que convertirla sea trivial.

Convertir en evento algo que necesita el resultado (conceptual). Qué pasa: alguien publica PriceRequested y espera que un suscriptor le deje el precio en algún lado, o hace publish y después lee un atributo que el manejador modificó. Funciona con un suscriptor y se rompe con dos, o peor, funciona en desarrollo y falla bajo carga. Por qué pasa: por arrastrar el modelo mental de la llamada a función. Un hecho informa, no pregunta. Cómo detectarlo: si tu código necesita saber qué hicieron los suscriptores, o si el evento se llama en imperativo o en interrogativo, no era un evento. Cómo corregirlo: llamada a función. Si de verdad hay varias implementaciones posibles de esa respuesta, lo que necesitas es una Strategy del módulo 3, no un evento.

Rechazar el punto medio por no tener nombre de patrón (de criterio). Qué pasa: en la revisión alguien propone extraer las reacciones a after_purchase.py y llamarla directo, y se descarta con un "eso sigue acoplado". Se elige el bus completo. Seis meses después el sistema tiene tres eventos con un suscriptor cada uno y un mapa que nadie mira. Por qué pasa: porque una solución con nombre —Observer— gana discusiones contra una sin nombre —una función—, aunque la segunda resuelva mejor el problema concreto. El vocabulario de patrones, que el módulo 1 presentó como un superpoder, tiene este lado oscuro: le da ventaja retórica a las opciones que están en el catálogo. Cómo detectarlo: si en la discusión nadie evaluó la opción intermedia, o se descartó sin medirla contra los síntomas concretos, hubo sesgo. Cómo corregirlo: pon los síntomas primero. Enumera los problemas reales —cuáles son, cuántas veces al año duelen— y evalúa cada opción contra esa lista, incluida la que no tiene nombre. El módulo 7 entero trata sobre usar el vocabulario sin que el vocabulario te use.

Ejercicios

Ejercicio 1 — Aplica las doce señales a cuatro casos. Para cada uno, di qué señales aplican y qué recomendarías: llamada directa, función que agrupa, o evento.

(a) Al cancelar una orden hay que liberar los boletos, devolver el dinero y avisarle al cliente. Los tres pasos son obligatorios; si uno falla, hay que resolverlo. Lo mantiene la misma persona.

(b) Al publicarse un evento nuevo en Boletia, hay que indexarlo para el buscador, avisar a los suscriptores del organizador, y publicar en redes sociales. El último lo pidió marketing la semana pasada; el equipo espera dos o tres integraciones más este año.

(c) Al registrarse un usuario hay que mandarle un correo de bienvenida. Nada más, desde hace tres años.

(d) Boletia quiere abrir una API de extensiones para que los organizadores enganchen sus propias automatizaciones cuando se vende un boleto.

Ver solución

(a) Llamada directa, sin dudarlo. Manda la señal D: los tres pasos son obligatorios y tienen que cuadrar entre sí. Además está la señal C —liberar los boletos antes de devolver el dinero probablemente importa— y la E, una sola persona mantiene todo. Esto no es un hecho con reacciones: es un procedimiento de cancelación con tres pasos, y se escribe como una función que los ejecuta en orden y falla ruidosamente si alguno falla. Convertirlo en evento sería el bug del inventario de la lección 3, multiplicado por tres.

(b) Evento. Señal 1 (tres interesados hoy), señal 2 (llegando seguido, con dos o tres más previstos y verificables porque están en el plan del trimestre), señal 3 (buscador, notificaciones y marketing son áreas distintas) y señal 4 (si la publicación en redes falla, el evento sigue publicado). Es el caso de libro. Y si el equipo es de seis personas y no se pisan, yo empezaría por el punto medio y convertiría a evento en cuanto llegue el cuarto interesado — el camino barato de la asimetría.

(c) Llamada directa, obviamente. Señal A en su forma más pura: un interesado, tres años estable. Un evento aquí son cuatro archivos y un concepto nuevo para reemplazar una línea. Si un compañero lo propone, la respuesta es: "cuando llegue el segundo lo hablamos, y convertirlo va a costar una tarde".

(d) Evento, y aquí es decisivo. Señal 5 en su forma fuerte: las extensiones no existen cuando se escribe el código del núcleo, así que es imposible que el núcleo las conozca. Esto ya no es una comodidad de diseño, es un requisito del producto. Nota además que este caso cambia todo lo demás: en el momento en que suscriptores de terceros pueden engancharse, el evento pasa a ser una API pública y las reglas de la lección 4 se vuelven obligatorias —versionado de verdad, nada de cambios que rompan, documentación de cada campo—. Un evento interno se puede cambiar en un despliegue; uno público, no.

El patrón general: los casos (a) y (c) son mayoría en el software real y casi nunca se discuten porque parecen aburridos. Los casos (b) y (d) son los que se cuentan en las charlas. Esa desproporción entre lo que se enseña y lo que se encuentra es la razón de ser de esta lección.

Ejercicio 2 — Escribe el comentario de revisión que frena un evento. Un compañero manda un cambio que agrega CustomerRegistered con un solo suscriptor, send_welcome_email. Escribe tu comentario de revisión: máximo cinco líneas, concreto, sin sonar a que estás bloqueando por gusto, y con una condición clara que lo desbloquearía.

Ver solución

Una versión que funciona:

"Con un solo suscriptor, el evento nos cuesta cuatro archivos y un concepto para hacer lo que hace send_welcome_email(customer) en una línea. Propongo llamarlo directo desde el registro, en una función after_registration() para que agregar el segundo interesado no toque el flujo de alta. Si ya sabemos de un segundo —CRM, analítica, lo que sea— lo hablamos y lo dejamos como evento desde ahora. Si no, convertirlo después nos va a costar una tarde y hoy nos ahorramos el mapa."

Fíjate en lo que hace ese comentario. Cuantifica el costo —cuatro archivos y un concepto— en vez de decir "es sobre-ingeniería", que suena a juicio. Propone una alternativa concreta, no solo un rechazo, y esa alternativa es el punto medio, que conserva casi todo el beneficio. Da la condición exacta que cambiaría la decisión —un segundo interesado real, con nombre—, así que la conversación deja de ser una cuestión de gusto. Y desactiva el miedo al costo futuro diciendo lo que cuesta convertirlo después: una tarde.

Lo que no hace, y es igual de importante: no dice "Observer no aplica aquí" ni cita ninguna regla. El módulo 1 ya lo advertía y el módulo 7 lo desarrolla: nombrar el patrón es vocabulario, no argumento. El argumento son los cuatro archivos y la tarde.

Ejercicio 3 — Convierte el punto medio en evento. Toma el run_after_purchase() de esta lección y escribe el plan de conversión a eventos: los pasos exactos, en orden, con la propiedad de que después de cada paso el sistema sigue funcionando. Después di cuánto tiempo estimas y compáralo con el camino inverso.

Ver solución

El plan, en pasos que se pueden desplegar por separado:

  1. Definir el hecho y el bus, sin usarlos todavía. Se agrega bus/bus.py, bus/events.py con OrderCompleted y sus pruebas. Nada del sistema los usa. Se puede desplegar; no cambia nada.
  2. Convertir cada reacción en una función que recibe el evento, sin dejar de llamarla desde run_after_purchase(). Es decir: cambia la firma, no el flujo. run_after_purchase construye el evento y llama a las cinco con él. Se despliega; el comportamiento es idéntico.
  3. Registrar las cinco en wiring.py y hacer que run_after_purchase() haga bus.publish(event) en vez de las cinco llamadas. Este es el único paso con riesgo real, y ahora está aislado: si algo sale mal, se revierte una línea.
  4. Borrar run_after_purchase() y publicar desde checkout.py directamente.
  5. Agregar las mitigaciones: describe(), la prueba del mapa, el identificador de traza.

Tiempo estimado: una tarde para los pasos 1 a 4, otra media para el 5. Y fíjate en la propiedad valiosa: después de cada paso el sistema funciona y se puede desplegar. Eso no es casualidad; es lo que se busca al planear un refactor, y el módulo 8 le dedica una lección entera.

El camino inverso —de eventos a llamada directa— tiene un paso cero que este no tiene: averiguar quién escucha. Con describe() son quince minutos; sin él, es una búsqueda por todo el repositorio y una decisión con dudas, porque nunca puedes estar seguro de haber encontrado a todos. Después vienen los mismos pasos, en reversa, pero cargando esa incertidumbre.

Por qué funciona: acabas de comprobar la asimetría con un plan concreto en vez de con un argumento. Ir hacia el evento es un camino conocido de principio a fin; volver empieza con una pregunta que el código no contesta. Esa diferencia es la razón técnica —no moral— para empezar simple.

Resumen y siguiente paso

En esta lección te pusiste la vacuna. Quitaste un evento real de Boletia —TicketTransferred, cuatro archivos y un solo suscriptor durante cuatro meses— y viste que agregar un evento es media hora sin riesgo mientras que quitarlo empieza con una pregunta que el código no contesta.

Te llevas doce señales verificables: seis a favor del evento —tres interesados o más, que aparezcan seguido, que vengan de módulos o equipos distintos, que no deban bloquear la operación, que el publicador no deba conocerlos por diseño, y que los interesados aparezcan y desaparezcan en tiempo de ejecución— y seis a favor de la llamada directa —uno o dos interesados estables, que necesites el resultado, que el orden importe, que deba ser la misma transacción, que los dos lados sean del mismo módulo, y que sea código que probablemente se borre—. Con una que manda sobre todas: si tiene que ocurrir en la misma transacción, no es un evento.

Viste el punto medio que casi nadie considera —extraer las reacciones a una función con nombre y llamarla directo— que resuelve tres de los cuatro síntomas del módulo en veinte minutos, sin conceptos nuevos y sin perder la trazabilidad. Y viste la asimetría: ir de llamada directa a evento es una tarde con toda la información a la vista; volver empieza a ciegas.

Antes de avanzar deberías poder: enumerar al menos ocho de las doce señales; explicar por qué la señal de la transacción manda sobre las demás; escribir el punto medio de memoria; y redactar en cinco líneas un comentario de revisión que frene un evento innecesario sin sonar a bloqueo.

Lo que viene es el proyecto, y es de decisión antes que de código. Vas a recibir el checkout de Boletia con sus reacciones y vas a tener que decidir, una por una, cuál merece ser evento, cuál se queda como llamada directa y cuál se va al punto medio —justificando cada decisión con las señales de esta lección—. Vas a entregar el código, el registro de qué escucha qué para no perder lo de la lección 6, y el documento de justificación. Y ese documento es, sin que lo parezca, la puerta al módulo 7: el momento en que los patrones dejan de ser cosas que implementas y pasan a ser el vocabulario con el que defiendes una decisión ante otra persona.

Recursos