Módulo 2: Cuándo NO abstraer
7. Acoplamiento contra indirección: elige tu veneno
Descripción
Al terminar esta lección vas a tener el tradeoff que estaba debajo de todo el módulo, y que hasta ahora habíamos visto por pedazos: quitar acoplamiento agrega indirección, y quitar indirección agrega acoplamiento. No hay una opción sin costo. Solo hay la opción correcta para este contexto. Vas a ver el espectro completo entre "llamada directa" y "registro dinámico" —son siete peldaños, no dos opciones— y vas a tener los tres ejes con los que se decide en cuál pararse: qué tan probable es el cambio, qué tan caro sería si llega, y cuánta gente lee ese código.
Esto importa porque es lo que convierte este módulo en criterio en vez de en una lista de reglas. Si sales de aquí pensando "menos abstracción es mejor", no aprendiste nada: cambiaste un sesgo por el sesgo contrario. Lo que quiero dejarte es la capacidad de mirar dos diseños, decir qué compra cada uno y qué cobra cada uno, y elegir con argumentos que otra persona pueda discutir.
Y hay una razón concreta, muy pegada a lo que acabamos de hacer. En la lección 6 desmontamos el registro de plugins, y al hacerlo el checkout pasó a depender directamente del código de asientos. Eso es acoplamiento. No lo eliminamos: lo cambiamos de sitio. Si no somos honestos sobre eso, el desmontaje parece un almuerzo gratis, y no lo es. Fue una compra, igual que la abstracción que quitamos. Simplemente era la compra correcta.
Conexión con el módulo: esta lección sube un nivel y muestra que las cinco anteriores eran el mismo tradeoff visto desde ángulos distintos. Los cuatro costos de la lección 2 son el precio de la indirección. La regla de tres de la lección 3 es un criterio para saber cuándo el eje 1 —la probabilidad del cambio— tiene evidencia. La lección 4 es lo que pasa cuando compras indirección en el eje equivocado. La 5 es lo que pasa cuando la compras sin que nadie te la haya pedido. Y la 6 es el caso extremo: pagar el máximo de indirección por cero acoplamiento evitado. La lección 8, el proyecto, te va a pedir justificar tu desmontaje exactamente en estos términos.
El enchufe y la regleta
Tienes una lámpara y quieres que dé luz. Hay varias maneras de conectarla a la corriente.
La primera: soldar el cable de la lámpara al cable de la pared. Funciona perfectamente. Máximo acoplamiento: la lámpara está unida a ese punto de esa pared para siempre. Y cero indirección: si un día no prende, hay exactamente un lugar donde mirar.
La segunda: clavija y enchufe. Un peldaño. Ahora puedes mover la lámpara a otro cuarto. A cambio, apareció un punto nuevo donde algo puede fallar —el contacto— y ahora "no prende" tiene dos causas posibles en vez de una.
La tercera: lámpara → clavija → extensión → regleta con interruptor → adaptador → enchufe. Muchísima flexibilidad: puedes mover la lámpara a donde quieras, apagarla desde la regleta, conectar cuatro cosas más. Y ahora "no prende" tiene cinco causas posibles, y para encontrar cuál es hay que revisarlas en orden, empezando por el interruptor de la regleta que alguien apagó sin querer con el pie.
Nadie defiende soldar los aparatos a la pared. Nadie defiende seis adaptadores en cadena tampoco. La discusión siempre es sobre cuántos peldaños, y la respuesta correcta depende de una sola cosa: qué tan seguido mueves la lámpara.
Si la mueves todas las semanas, la regleta se paga sola. Si lleva ocho años en la misma esquina, cada eslabón es un punto de falla que no compró nada.
Guarda esa imagen, porque el resto de la lección es hacerla precisa.
Los dos venenos, con su anatomía
Vamos a definir los dos términos con cuidado. El primero se da por sabido en esta guía —viene de las fundaciones— pero conviene desarmarlo, porque la palabra se usa como si fuera una sola cosa y son cuatro.
Acoplamiento
A está acoplado a B cuando un cambio en B puede obligar a un cambio en A.
Y viene en grados, de menos a más molesto:
| Grado | Qué significa | Ejemplo en Boletia |
|---|---|---|
| Por nombre | A menciona a B | checkout importa send_email |
| Por firma | A conoce los parámetros y el retorno de B | checkout sabe que send_email recibe tres strings |
| Por dato | A conoce el formato o las unidades de lo que B maneja | El checkout sabe que Stripe quiere centavos enteros |
| Por tiempo | A depende de cuándo ocurre B | El checkout supone que el cobro termina antes de responderle al cliente |
Nota que el acoplamiento no es malo por definición. El checkout tiene que estar acoplado a algo que cobre; si no, no cobra. Lo que se puede elegir es a qué está acoplado y en qué grado. El acoplamiento por nombre a una función con un buen nombre es barato. El acoplamiento por dato a las unidades internas de una API externa es caro, porque esa API cambia sin avisarte.
Indirección
Indirección es que para llegar de A a B haya que pasar por C, donde C no hace el trabajo: reenvía, decide o traduce.
Su anatomía tiene tres partes, y las tres cuestan:
- C tiene que existir. Es código que se escribe, se lee, se testea y se mantiene.
- Alguien tiene que decidir qué hay detrás de C. Esa decisión se muda a otro lugar, y ese lugar es nuevo.
- Para seguir el flujo hay que atravesar C. Es el salto de la lección 2, con su costo en memoria de trabajo.
La conservación
Ahora la idea central, y es tan simple que se pasa por alto.
Si checkout llama a assign_seat directamente, está acoplado a ella y no hay indirección. Si metes una interfaz en medio, checkout ya no conoce assign_seat… pero ahora conoce la interfaz. Y además alguien, en algún lugar, tiene que decidir qué implementación va detrás. Ese lugar es nuevo y concentra el conocimiento que le quitaste al checkout.
El acoplamiento no desapareció: se movió. Y el precio de moverlo fue un salto.
Esto reformula la pregunta que la gente se hace, y la reformulación es lo más útil de la lección:
No preguntes "¿cómo elimino el acoplamiento?". Pregunta "¿dónde quiero que viva?".
Porque en algún lado tiene que vivir. La única manera de que un sistema no tenga acoplamiento es que sus partes no se comuniquen, y entonces no es un sistema.
Y hay un corolario que conviene tener a mano cuando alguien use la palabra como bandera: "código desacoplado" no significa nada por sí solo. Un diagnóstico útil siempre tiene tres partes: A está acoplado a B por X, y eso importa porque B cambia con frecuencia. Sin las tres, es una etiqueta.
Ejemplo trabajado: Boletia le avisa al organizador
El requisito: cuando alguien compra un boleto, además de avisarle al cliente hay que avisarle al organizador del evento. Vamos a resolverlo en tres peldaños distintos y a medir cada uno.
Peldaño 1 — llamada directa desde el checkout.
# Archivo: checkout/checkout.py
from notifications.email_channel import send_email
def checkout(order):
...
organizer = load_customer(event.organizer_id)
send_email(
organizer.email,
"Nueva venta en Boletia",
f"Se vendió 1 boleto de {event.name}.",
)
Acoplamiento: alto y de varios grados. El checkout sabe que existe un organizador, sabe cómo obtenerlo, sabe que se le avisa por correo, y conoce el texto del mensaje. Indirección: cero. Un archivo, cero saltos, todo visible en el sitio.
Peldaño 3 — una función de intención.
# Archivo: notifications/notifier.py
def notify_purchase(order, event, customer):
"""Único lugar que decide a quién se le avisa de una compra y por dónde."""
send_email(customer.email, "Tu compra en Boletia", build_confirmation_email(order))
if customer.phone: # no todos los clientes dieron teléfono
send_sms(customer.phone, build_confirmation_sms(order))
if customer.push_token: # ni todos tienen la app instalada
send_push(customer.push_token, build_confirmation_push(order))
organizer = load_customer(event.organizer_id)
send_email(organizer.email, "Nueva venta en Boletia", f"Se vendió 1 boleto de {event.name}.")
# Archivo: checkout/checkout.py
from notifications.notifier import notify_purchase
def checkout(order):
...
notify_purchase(order, event, customer)
Acoplamiento: el checkout sabe que hay que notificar —lo cual es correcto, es parte del negocio— pero ya no sabe a quién ni por dónde. Ese conocimiento se mudó a notifier.py. Indirección: un salto, a un archivo con un nombre que dice exactamente qué hay adentro.
Peldaño 6 — publicación de eventos con suscriptores.
# Archivo: events/bus.py
from collections import defaultdict
_SUBSCRIBERS: dict[str, list] = defaultdict(list)
def subscribe(event_name: str, handler):
_SUBSCRIBERS[event_name].append(handler)
def publish(event_name: str, payload: dict):
for handler in _SUBSCRIBERS[event_name]:
handler(payload)
# Archivo: checkout/checkout.py
from events.bus import publish
def checkout(order):
...
publish("purchase_completed", {"order_id": order.id})
Acoplamiento: mínimo. El checkout no sabe que existen las notificaciones. Indirección: máxima.
Las tres formas producen el mismo comportamiento. Comparémoslas:
| Peldaño 1: directo | Peldaño 3: función de intención | Peldaño 6: eventos | |
|---|---|---|---|
| Acoplamiento del checkout | A los canales, a los destinatarios y a los textos | A "hay que notificar", nada más | A un nombre de evento |
| Saltos para responder "¿qué pasa al comprar?" | 0 | 1 | Indeterminado: hay que buscar los subscribe en todo el proyecto |
| Agregar un cuarto interesado (taquilla) | Editar el checkout | Editar notifier.py | Agregar un subscribe en cualquier archivo |
| ¿Se sabe el orden de ejecución? | Sí, se lee | Sí, se lee | No: depende del orden de registro |
| Si un aviso falla, ¿siguen los demás? | Lo decides ahí | Lo decides ahí | Hay que diseñarlo aparte |
| Modos de falla nuevos | 0 | 0 | Suscriptor no registrado, orden inesperado, fallo silencioso |
Qué esperar de esta comparación. Cuatro conclusiones, y la última es la que importa.
Primera: el peldaño 3 es el correcto para Boletia hoy, y no porque sea el del medio. Es el correcto porque los tres ejes que veremos en un momento apuntan ahí: la lista de interesados ya cambió tres veces en un año (el cambio es probable), cambiarla cuesta editar un archivo pequeño (el cambio es barato), y el checkout lo abren seis personas cada semana (mucha gente lee ese código, así que la indirección ahí es cara).
Segunda: el peldaño 1 no está mal en abstracto. Está mal para este caso. Si el aviso al organizador fuera lo único que se notifica y nunca hubiera cambiado, la llamada directa sería la mejor opción posible: cero saltos, todo visible. La razón por la que no lo es aquí es que ese bloque ya creció tres veces, y cada vez obligó a tocar el corazón del sistema.
Tercera: el peldaño 6 tampoco está mal en abstracto. Está mal para un equipo de seis personas con un solo proceso. Su beneficio —que quien publica no sabe quién escucha— se cobra cuando los que escuchan son muchos, cambian seguido, o viven en otro despliegue. Su costo —que ya no puedes seguir el flujo con el dedo— se paga siempre. El módulo 6 de esta guía enseña Observer en serio y ahí vas a ver los casos donde el peldaño 6 gana. Aquí lo que importa es que veas que el peldaño 6 también tiene precio, porque casi nunca se enseña con él.
Cuarta, y es el punto: ninguna de las tres columnas es "la buena". Las tres tienen la misma estructura de costos —compran algo, cobran algo— y la elección depende de números del contexto, no de principios. Cuando alguien te diga que una de estas formas es "la correcta" sin preguntar por el contexto, te está vendiendo un gusto personal con nombre técnico.
El espectro completo: siete peldaños
La discusión sobre acoplamiento casi siempre se plantea como si hubiera dos opciones —"acoplado" o "desacoplado"— y eso es lo que produce el salto que vimos en la lección 6: alguien decide que hay que desacoplar y salta directo del peldaño 2 al 7.
Hay siete peldaños, y el correcto casi siempre es el segundo o el tercero.
| # | Forma | Acoplamiento | Indirección | Dónde vive la decisión |
|---|---|---|---|---|
| 1 | Código en línea, en el sitio de uso | Máximo | Ninguna | En el sitio |
| 2 | Llamada a una función con nombre | Alto | 1 salto | En el sitio |
| 3 | Función de intención que agrupa la decisión | Medio | 1 salto | En un módulo con nombre |
| 4 | La dependencia se recibe como parámetro | Medio-bajo | 1–2 saltos | En quien construye |
| 5 | Interfaz + diccionario literal de opciones | Bajo | 2 saltos | En un diccionario visible |
| 6 | Interfaz + fábrica que lee configuración | Bajo | 3 saltos | En un archivo de configuración |
| 7 | Registro dinámico, descubrimiento o eventos | Mínimo | 4+ saltos | En ningún lugar visible |
Fíjate en la última columna, porque es la que mejor describe el intercambio: cada peldaño que bajas aleja la decisión del lugar donde se usa. En el peldaño 1 la decisión está escrita en la línea que la ejecuta. En el peldaño 7 no está escrita en ninguna parte: emerge del estado del sistema en tiempo de ejecución, del orden de los imports y del contenido de una carpeta.
Eso es exactamente lo que quieres cuando la decisión tiene que poder tomarse sin tocar código —porque la toma un cliente, o un operador, o un despliegue distinto—. Y es exactamente lo que no quieres cuando la decisión la toma tu equipo y no cambia nunca.
Un par de observaciones prácticas sobre la tabla:
El peldaño 5 está muy infravalorado. Un diccionario literal que mapea un nombre a una función resuelve la enorme mayoría de los casos que la gente resuelve con un registro dinámico, y cabe en diez líneas que se leen de un vistazo:
# Archivo: seating/strategies.py (el peldaño 5, si algún día hicieran falta tres algoritmos)
STRATEGIES = {
"first_free": assign_first_free,
"best_available": assign_best_available,
"contiguous_group": assign_contiguous_group,
}
def assign_seat(ticket, event):
strategy = STRATEGIES[event.seating_strategy]
return strategy(ticket, event)
Compara eso con las ciento ochenta y tres líneas de la lección 6. Mismo desacople efectivo para tres algoritmos internos, y la decisión sigue estando visible en un archivo que puedes leer entero. La diferencia entre el peldaño 5 y el 7 no es de grado: es que en el 5 la lista de opciones existe como texto y en el 7 no.
Subir es barato; bajar es un proyecto. Ir del peldaño 2 al 3 es extraer una función: minutos, reversible. Ir del 7 al 2 es lo que hiciste en la lección 6: seis pasos, tests de caracterización y una migración de esquema aparte. Esa asimetría tiene una consecuencia operativa directa:
Empieza en el peldaño más bajo que resuelva el problema de hoy, y sube uno cuando la evidencia lo pida.
Los tres ejes de la decisión
Ahora el instrumento. Cuando tengas que elegir peldaño, puntúa tres ejes.
Eje 1 — ¿Qué tan probable es el cambio?
La pregunta es fácil de contestar mal, porque la respuesta intuitiva siempre es "podría cambiar". Todo podría cambiar. Lo que necesitas es evidencia, y la tienes a mano:
# ¿Cuántas veces cambió este archivo en el último año?
git log --since="1 year ago" --oneline -- boletia/notifications/notifier.py | wc -l
El historial es el mejor estimador que existe de la volatilidad futura, y tiene la ventaja de que no se discute. Si la lista de interesados en una compra cambió tres veces en doce meses, el eje 1 está alto y no hay nada que debatir. Si el código de asientos no cambió en dos años, el eje 1 está bajo y tampoco.
Cuidado con un matiz: lo que importa es la volatilidad del eje concreto, no del archivo. Que checkout.py cambie todas las semanas no significa que la forma de asignar asientos sea volátil; significa que el checkout hace muchas cosas.
Eje 2 — ¿Qué tan caro sería el cambio si llega?
No es "cuántas horas". Es un conjunto de preguntas sobre el alcance:
- ¿Cuántos archivos hay que tocar?
- ¿Toca el corazón del sistema —el lugar donde nadie quiere equivocarse—?
- ¿Requiere migrar datos?
- ¿Hay que coordinar con gente fuera de tu control: otros equipos, clientes, aplicaciones ya publicadas?
Las dos últimas son las que disparan este eje. Un cambio que solo toca código de tu repositorio es barato aunque toque veinte archivos, porque lo controlas tú y lo puedes revertir. Un cambio que toca datos o terceros es caro aunque sea de una línea.
Eje 3 — ¿Cuánta gente lee ese código?
Este es el que más se olvida y el que más asimétrico hace todo. La indirección se paga por lector. Un rincón que abre una persona una vez al año tolera muchísima más indirección que el checkout, que abren seis personas cada semana.
Y hay una versión extendida del eje que cambia la respuesta por completo: ¿quién puede depender de esto? Si la respuesta incluye a alguien fuera de tu repositorio, el eje 2 se dispara junto con este, y la cuenta entera cambia. Lo vimos en la lección 2 y lo vamos a ver en el último ejercicio.
Y un cuarto eje que decide los empates
¿Qué tan reversible es la decisión? Ya lo tienes de la lección 5. Si puedes subir o bajar un peldaño en una tarde, no deliberes: elige el peldaño bajo y ajusta cuando haya evidencia. Deliberar cuesta más que equivocarse.
Este eje es el que hace que la regla operativa funcione. Empezar bajo es seguro precisamente porque subir es barato.
La puntuación aplicada a cuatro rincones de Boletia
| Rincón | Eje 1: probabilidad | Eje 2: costo si llega | Eje 3: lectores | Peldaño |
|---|---|---|---|---|
| Asignación de asientos | Bajo (0 cambios en 2 años) | Bajo (un archivo) | Bajo (nadie lo abre) | 2 — función directa |
| Interesados en una compra | Alto (3 cambios en 1 año) | Medio (toca el checkout) | Alto (6 personas/semana) | 3 — función de intención |
| Proveedores de pago | Medio (3 en 3 años) | Alto (dinero, APIs externas) | Alto | 5 — interfaz + diccionario |
| Formato de moneda | Bajo | Bajo | Medio | 2 — función directa |
Mira la fila de los proveedores de pago, porque es la que muestra que los ejes no siempre apuntan igual. El eje 1 es solo medio —tres proveedores en tres años—, pero el eje 2 está alto: son APIs externas incompatibles, hay dinero de por medio, y equivocarse ahí es caro de verdad. Eso solo ya justifica subir hasta el peldaño 5. Un eje alto puede decidir por sí solo; no es un promedio.
Y mira la primera fila junto con la última de la lección 6: los tres ejes bajos, y había un peldaño 7 puesto ahí.
El mismo código, dos contextos, dos respuestas correctas
Para cerrar, el experimento que mejor demuestra que no existe "el diseño correcto".
Tomemos la función format_money de la lección 2. Cinco líneas, una moneda, cero abstracción. Dijimos que la versión con interfaz y fábrica era peso muerto, y lo era.
Contexto A: dentro del servicio de Boletia. Seis personas, un repositorio, un despliegue. Si mañana hace falta otra moneda, alguien edita la función y despliega. Los tres ejes: probabilidad baja, costo bajo, lectores pocos. Respuesta correcta: peldaño 2, función suelta.
Contexto B: la misma función, pero publicada como parte de una librería interna que consumen cuarenta equipos de una empresa grande, con versiones publicadas y aplicaciones que ya están en producción.
Ahora vuelve a puntuar. El eje 1 no cambió: sigue siendo poco probable que haga falta otra moneda. El eje 3 se disparó: la leen cuarenta equipos. Y el eje 2 se disparó más: si mañana hay que cambiar la firma, no puedes editarla y desplegar. Tienes que publicar una versión nueva, coordinar cuarenta migraciones, mantener la versión vieja mientras tanto, y aceptar que algunos equipos no van a actualizar en un año.
Respuesta correcta: un punto de extensión desde el principio —aunque hoy haya una sola moneda—, porque el costo de agregarlo después no es "un poco más": es coordinar con cuarenta equipos que no controlas.
El mismo código. Dos respuestas opuestas. Las dos correctas. Lo único que cambió fue el contexto, y el contexto es enunciable: se puede escribir en dos líneas y discutir.
De ahí sale la formulación que quiero que te lleves del módulo entero:
No existe "el diseño correcto". Existe el diseño correcto para este contexto, y el contexto se puede enunciar: qué tan probable es el cambio, qué costaría si llega, y quién lee o depende de este código.
Y de ahí sale también algo práctico para tu trabajo diario: cuando leas un consejo de diseño en internet —un artículo, una respuesta en un foro, una charla— la primera pregunta útil no es si estás de acuerdo. Es en qué contexto lo escribieron. Buena parte de las discusiones eternas sobre arquitectura son dos personas con razón hablando de contextos distintos.
Errores comunes
Usar "acoplado" como diagnóstico completo (de comunicación). Qué pasa: en una revisión, alguien escribe "este código está muy acoplado" y espera que eso baste. La otra persona no sabe qué cambiar, porque todo código está acoplado a algo. La conversación se estanca o, peor, produce un desacople al azar que agrega tres saltos sin quitar ningún riesgo real. Por qué pasa: la palabra tiene carga negativa y suena a diagnóstico técnico, así que se siente suficiente. Cómo detectarlo: si tu comentario no dice a qué está acoplado y por qué eso importa, no es un diagnóstico. Cómo corregirlo: usa la forma de tres partes —A está acoplado a B por X, y eso importa porque B cambia con frecuencia / porque B es externo / porque un cambio en B obliga a tocar el corazón del sistema—. Con las tres partes, quien recibe el comentario sabe exactamente qué hacer, y además puede estar en desacuerdo con evidencia. El módulo 7 se dedica entero a esta habilidad.
Estimar el eje 1 con la intuición en vez de con el historial (de método). Qué pasa: alguien afirma que algo "va a cambiar seguro" y sobre esa afirmación se justifica subir tres peldaños. Nadie la verifica, porque suena razonable y porque desmentirla parece pesimismo. Dos años después el archivo no cambió ni una vez. Por qué pasa: la volatilidad futura se siente estimable y no lo es; y hay un sesgo específico —lo que estás mirando ahora te parece más importante y más cambiante de lo que es—. Cómo detectarlo: si en la discusión nadie miró el historial, el eje 1 no está estimado, está imaginado. Cómo corregirlo: git log sobre el archivo o la carpeta, restringido al último año. Es un comando y cambia discusiones enteras. Y guarda esta forma de decirlo, que funciona sin confrontar: "antes de decidir, ¿miramos cuántas veces cambió esto el año pasado?".
Saltar del peldaño 2 al 7 (de calibración). Qué pasa: alguien detecta —correctamente— que hay demasiado acoplamiento, y como el espectro que tiene en la cabeza es binario, salta directo a la solución máxima: registro dinámico, eventos, plugins. El problema real se resolvía en el peldaño 3 o 5, y ahora hay un rincón que nadie entiende. Por qué pasa: el vocabulario de la industria es binario —"acoplado" contra "desacoplado"— y los peldaños intermedios no tienen nombres famosos. Un diccionario literal no aparece en ningún catálogo de patrones; un registro de plugins sí. Cómo detectarlo: si tu solución agrega más de dos saltos, párate y pregúntate qué peldaño intermedio probaste. Cómo corregirlo: ten la tabla de los siete peldaños presente y recorre de abajo hacia arriba. La pregunta operativa es "¿cuál es el peldaño más bajo que resuelve esto?", no "¿cómo lo desacoplo del todo?". Y recuerda la asimetría: subir es barato, bajar es un proyecto.
Ejercicios
Ejercicio 1 — Ubica cinco fragmentos en el espectro. Para cada uno, di en qué peldaño está y cuál sería el peldaño inmediatamente superior e inferior.
(a) checkout.py importa stripe y llama a stripe.Charge.create(...) directamente.
(b) checkout.py llama a charge_order(order, provider_name), y esa función tiene un if/elif por proveedor.
(c) checkout.py recibe un objeto payment_provider como parámetro y llama a payment_provider.start_payment(order).
(d) checkout.py llama a get_provider(order.provider).start_payment(order), donde get_provider consulta un diccionario literal de tres entradas.
(e) checkout.py publica un evento "order_ready_to_charge" y algún suscriptor cobra.
Ver solución
(a) Peldaño 1. Código de un proveedor concreto en el sitio de uso, con acoplamiento por dato incluido: el checkout sabe que Stripe quiere centavos. Abajo no hay nada; arriba está (b).
(b) Peldaño 2 y medio. Hay una función con nombre, pero la decisión sigue siendo un condicional visible. Es el estado que M1 describe para Boletia. Abajo está (a); arriba está (d).
(c) Peldaño 4. La dependencia se recibe de fuera. El checkout no decide nada: quien lo llama decide. Ojo con un detalle que confunde: esto parece más desacoplado que (d), y en cierto sentido lo es, pero la decisión no desapareció — se mudó a quien construye el checkout, que ahora tiene que saber elegir proveedor. Si ese "quien construye" es el manejador HTTP, acabas de mover una decisión de negocio a la capa de transporte, que probablemente sea peor. Abajo está (b); arriba está (d).
(d) Peldaño 5. Interfaz más diccionario literal. La lista de opciones existe como texto en un archivo y se lee de un vistazo. Para Boletia, con tres proveedores y dinero de por medio, este es el peldaño correcto. Abajo está (c); arriba está (f), que sería leer el diccionario desde configuración.
(e) Peldaño 7. Y aquí conviene notar algo que casi nadie dice: esto no solo agrega indirección, cambia la semántica del negocio. Si el cobro ocurre en un suscriptor, ¿el checkout le responde al cliente antes de saber si el pago funcionó? ¿Qué pasa si el suscriptor falla? ¿Y si no hay ninguno registrado? Cobrar es la operación menos apta para desacoplarse por evento de todo el sistema, porque el resultado le importa inmediatamente a quien la pidió.
Por qué funciona: el ejercicio te obliga a ver que entre "llamada directa" y "eventos" hay cinco escalones intermedios con nombres y propiedades. Y (e) agrega una lección extra: los peldaños altos no solo cuestan saltos, a veces cambian qué garantías puede dar tu sistema.
Ejercicio 2 — Puntúa los tres ejes y recomienda peldaño. Para cada caso de Boletia, puntúa los tres ejes (alto / medio / bajo), justifica cada puntuación con la evidencia que usarías, y recomienda un peldaño.
(a) La generación del código QR del boleto. Se hace con una librería, no ha cambiado desde que existe, y la usan dos archivos. (b) Las reglas de precio por tipo de boleto. Cambiaron cinco veces en el último año porque marketing hace promociones, y viven en el camino del checkout. (c) La conexión a la base de datos.
Ver solución
(a) Código QR. Eje 1: bajo — evidencia: git log sobre el archivo, cero cambios desde su creación. Eje 2: bajo — si hubiera que cambiar de librería, se toca un archivo y no hay datos ni terceros de por medio. Eje 3: bajo — dos sitios de llamada, nadie lo abre a leer. Peldaño 2: una función generate_qr(ticket) y ya. Nada de interfaz QrGenerator. Este es exactamente el perfil del rincón de asientos: los tres ejes abajo.
(b) Reglas de precio. Eje 1: alto — cinco cambios en un año, y la causa es estructural: marketing va a seguir haciendo promociones. Eje 2: medio-alto — es dinero, está en el camino del checkout, y un error cobra de más o de menos a un cliente real. Eje 3: alto — es de los rincones que más se leen, porque cada promoción nueva obliga a entenderlo. Peldaño 5: una interfaz y un diccionario que mapee tipo de boleto a regla. Aquí una abstracción se gana su lugar con claridad, y hay cuatro implementaciones reales. Es el terreno del módulo 3.
(c) Conexión a base de datos. Eje 1: bajo — casi nadie cambia de motor. Eje 2: muy alto — si llegara a pasar, toca todo el sistema, hay migración de datos, y no es reversible. Eje 3: alto — todo el mundo la toca indirectamente. Peldaño 4 o 5, y aquí está lo interesante: el eje 1 es bajo y aun así se justifica subir, por el eje 2 solo. Es el caso que demuestra que los ejes no se promedian. Y el argumento adicional que decide: casi siempre hay una segunda implementación real —la de los tests— así que ni siquiera aplica el olor de la lección 6.
Por qué funciona: los tres casos tienen perfiles de eje distintos y llegan a peldaños distintos, uno de ellos por un solo eje. Si la recomendación fuera siempre "peldaño bajo", no necesitarías tres ejes: necesitarías un prejuicio.
Ejercicio 3 — Dos contextos, dos respuestas. Toma la función assign_seat(ticket) que quedó después del desmontaje de la lección 6. Describe un contexto en el que esa función suelta sea la respuesta correcta —el actual— y un contexto distinto en el que la respuesta correcta sea un mecanismo de extensión con carga dinámica. Di explícitamente qué eje cambia entre los dos contextos, y escribe el enunciado del contexto en dos líneas, como lo pondrías en un documento de diseño.
Ver solución
Contexto A (el actual): Boletia es un producto único, desplegado por un solo equipo de seis personas, con una sola forma de asignar asientos que no ha cambiado en dos años. Nadie fuera del repositorio depende de esta función.
Ejes: 1 bajo, 2 bajo, 3 bajo. Respuesta: función suelta, peldaño 2. Es lo que hicimos.
Contexto B: Boletia se vende como plataforma instalable a recintos que la despliegan en su propia infraestructura. Tres de ellos tienen sistemas de butacas heredados y necesitan conectar su propia lógica de asignación sin que nosotros publiquemos una versión. Hay un contrato firmado con dos de ellos.
Ejes: 1 alto (hay tres implementaciones reales pedidas), 2 muy alto (el código lo escribe gente fuera de tu control y no puedes desplegar por ellos), 3 alto. Respuesta: mecanismo de extensión con carga dinámica, peldaño 7. Exactamente lo que había.
El eje que cambia es el 2, en su versión extendida: ¿quién puede depender de esto y a quién hay que coordinar para cambiarlo?. En el contexto A la respuesta es "nosotros"; en el B es "tres recintos con contrato que despliegan por su cuenta". Ese solo cambio justifica cuatro peldaños de diferencia.
Fíjate en una consecuencia incómoda y muy útil: el código de plugins/ que desmontamos era el diseño correcto para un producto que Boletia nunca fue. No estaba mal escrito ni mal concebido en abstracto; estaba resolviendo el contexto B en una empresa que vivía en el contexto A. Cuando revises código ajeno, esa es una hipótesis que vale la pena tener a mano: mucha sobre-ingeniería es un diseño correcto para un contexto equivocado, muchas veces copiado de un artículo o de un empleo anterior donde ese contexto sí existía.
Por qué funciona: el ejercicio cierra el módulo con la idea que lo ordena. La pregunta nunca fue "¿abstraer o no?". Fue "¿cuál es el contexto?", y el contexto se enuncia en dos líneas que cualquiera puede discutir. Ese enunciado es, además, la primera sección de tu entrega del proyecto.
Resumen y siguiente paso
En esta lección viste el tradeoff que estaba debajo de todo el módulo. Quitar acoplamiento agrega indirección; quitar indirección agrega acoplamiento. No hay opción sin costo. El acoplamiento no se elimina: se mueve, y el precio de moverlo es un salto. Por eso la pregunta útil no es "¿cómo elimino el acoplamiento?" sino "¿dónde quiero que viva?".
Desarmaste los dos términos. El acoplamiento viene en cuatro grados —por nombre, por firma, por dato, por tiempo— y no es malo por definición: lo que eliges es a qué estás acoplado y en qué grado. La indirección cuesta en tres partes: el código que hay que escribir, la decisión que se muda a otro lugar, y el salto que hay que atravesar para leer.
Viste el mismo requisito de Boletia —avisarle al organizador— resuelto en tres peldaños, con sus costos medidos, y comprobaste que ninguna de las tres columnas es "la buena": las tres compran algo y cobran algo.
Y sobre todo tienes el espectro de siete peldaños en lugar del binario "acoplado / desacoplado", con la observación que lo ordena: cada peldaño que bajas aleja la decisión del lugar donde se usa, hasta que en el séptimo la decisión ya no está escrita en ninguna parte. Más el peldaño 5, el más infravalorado de todos: un diccionario literal resuelve casi todo lo que la gente resuelve con un registro dinámico, y se lee de un vistazo.
Tienes los tres ejes —probabilidad del cambio (que se estima con el historial, no con la intuición), costo del cambio si llega, y cuánta gente lee o depende de ese código— más el cuarto que decide los empates: la reversibilidad. Y la regla operativa que sale de todo: empieza en el peldaño más bajo que resuelva el problema de hoy y sube cuando la evidencia lo pida, porque subir es barato y bajar es un proyecto.
Y te llevas la formulación que resume el módulo: no existe el diseño correcto; existe el diseño correcto para este contexto, y el contexto se puede enunciar en dos líneas.
Antes de avanzar deberías poder: explicar por qué "código desacoplado" no es un diagnóstico completo; nombrar al menos cinco de los siete peldaños; puntuar los tres ejes sobre un rincón concreto usando evidencia; y describir dos contextos en los que el mismo código tenga dos respuestas correctas distintas.
Ya tienes las siete piezas del criterio. Lo que falta es usarlas. La lección 8 es el proyecto: vas a tomar el rincón plugins/ de Boletia, desmontarlo hasta dejar el código directo que hace exactamente lo mismo, y entregar el antes y el después más la justificación —qué costaba, qué se gana, y bajo qué condición concreta habría que volver a ponerlo—. Se juzga por el razonamiento y por no haber roto el comportamiento, no por la cantidad de líneas borradas.
Recursos
- Simple Made Easy (Rich Hickey) — la charla que separa "simple" (pocas cosas entrelazadas) de "fácil" (familiar). Es la mejor pieza sobre por qué desacoplar y simplificar no son lo mismo, y por qué a veces desacoplar complica.
- Structured Design (Constantine y Yourdon) — el origen de la escala de grados de acoplamiento. Es de los años setenta y sus categorías siguen siendo las que usamos; conviene conocer que el vocabulario tiene medio siglo.
- The Grug Brained Developer — su tratamiento de "factoring" es, en el fondo, esta lección: el problema no es acoplar o desacoplar, es dónde poner las costuras.
- Write code that is easy to delete, not easy to extend (tef) — plantea el espectro desde otro ángulo: en vez de preguntarte cuánto desacople quieres, pregúntate cuánto te costaría borrar esto. Las dos preguntas llevan al mismo lugar.