Módulo 5: Patrones para estructurar y adaptar

1. Presentación del módulo: envolver, adaptar, simplificar

Descripción

Al terminar esta lección vas a tener tres cosas. Primero, el problema que une a esta familia de patrones, dicho en una frase que vas a poder repetir de memoria: el contacto con lo que no controlas. Segundo, vas a conocer al villano del módulo con nombre y apellido —el SDK del proveedor de pago que Boletia integró el año pasado, con sus nombres inconsistentes, sus errores raros y sus campos que a veces vienen y a veces no— y vas a ver con tus propios ojos dónde está desparramado su desorden hoy: cinco archivos que no tenían por qué saber nada de él. Y tercero, vas a tener el mapa de los cuatro patrones del módulo con la frase de una línea que los distingue, más la pregunta incómoda que los acompaña a todos: ¿no bastaba con escribir una función que envuelva?

Esto importa por una razón que se vuelve evidente el día que entras a mantener software que ya lleva años en producción. Los módulos 3 y 4 trabajaron sobre código tuyo: las reglas de precio de Boletia, sus proveedores de pago, sus exportadores de reportes. Cuando algo no encajaba, podías cambiarlo. Este módulo trabaja sobre la otra mitad del oficio, que es la mitad que nadie enseña: el código que no puedes cambiar. La librería de un tercero. El SDK que publica el proveedor. El módulo viejo que escribió alguien que ya no trabaja aquí y que nadie se atreve a tocar. Ese código tiene sus propias ideas sobre cómo se nombran las cosas, sobre qué es un error y sobre en qué unidades se mide el dinero, y ninguna de esas ideas te consultó.

Los patrones de esta familia son las cuatro formas conocidas de manejar ese contacto. No hacen mejor al código ajeno —eso no está a tu alcance—. Lo que hacen es poner una frontera: un lugar preciso donde su desorden se traduce al tuyo y no pasa de ahí. Y porque el código real siempre tiene un vecino desprolijo, esta familia es, con diferencia, la que más aparece en sistemas de verdad. También es la que más se aplica de más, y por eso el módulo trae su propia vacuna en la lección 7.

Conexión con el módulo: esta lección es el marco y el diagnóstico; todavía no vas a escribir ningún patrón. Aquí vas a ver el SDK desprolijo tal como es y vas a medir el daño que hace hoy. La lección 2 define Adapter, el traductor, con su anatomía desarmada, y explica por qué es el patrón con mejor relación valor/costo en código real. La lección 3 hace el caso completo: envuelve el SDK detrás de una interfaz propia y limpia, y mide qué se gana. La lección 4 pasa a Facade, que resuelve un problema distinto —no traducir, sino simplificar— y trae el riesgo de la fachada que crece hasta volverse otro God object. La lección 5 es Decorator: agregar reintento, registro o caché a algo sin modificarlo. La lección 6 es Composite, el patrón de las estructuras en árbol, con la advertencia de qué pasa cuando lo fuerzas donde no hay árbol. La lección 7 es el freno: las tres preguntas que deciden si hacía falta una clase o bastaba una función de tres líneas. Y la lección 8 —el proyecto— te pide aislar la integración con el proveedor y entregar, junto al código, la justificación de dónde pusiste la frontera y por qué ahí.

El enchufe de otro país

Llegas a un hotel en otra ciudad con tu cargador en la mano y descubres que la pared tiene tres agujeros en vez de dos, o dos agujeros redondos en vez de planos. Tu cargador funciona perfectamente. La pared funciona perfectamente. Simplemente no se entienden.

Nadie, en esa situación, intenta rediseñar la pared del hotel. Y nadie abre el cargador para soldarle otras patas. Lo que haces es comprar un adaptador de tres dólares: una pieza que por un lado tiene la forma de la pared y por el otro tiene la forma de tu cargador, y que no hace absolutamente nada más. No transforma la electricidad, no mejora la carga, no tiene opinión. Solo traduce una forma en otra.

Fíjate en cuatro cosas de ese adaptador, porque son exactamente las cuatro cosas que vas a necesitar del patrón.

Es pequeño. No tiene lógica. Si tuviera un botón, o un modo, o una configuración, ya no sería un adaptador.

Está en un solo lugar. Compras uno y resuelves el problema para todos tus aparatos. No necesitas un adaptador dentro de cada dispositivo.

Es reemplazable. Cuando viajas a otro país, cambias el adaptador y no cambias ni un aparato. Esta es la propiedad que en software vale dinero: cambiar de proveedor sin tocar tu lógica.

Y es honesto sobre lo que no puede hacer. Un adaptador de forma no convierte 220 voltios en 110. Si tu aparato no aguanta el voltaje, el adaptador no lo salva; para eso hace falta un transformador, que es otra cosa. En software pasa igual y conviene decirlo desde ahora: un Adapter traduce formas, no arregla diferencias de fondo. Si el SDK del proveedor no sabe hacer reembolsos parciales, ningún patrón te los va a inventar.

Ahora, la parte de la analogía que casi nunca se cuenta y que es la más útil de este módulo. Imagina que en vez de comprar un adaptador, cada persona de tu familia hubiera resuelto el problema por su cuenta: uno le cortó las patas al cargador con unas pinzas, otro dobló el enchufe de la lámpara, otro consiguió una extensión rarísima. Cada solución funciona. Ninguna está escrita en ningún lado. Y el día que se muden a otro país, hay que revisar aparato por aparato para ver qué le hicieron a cada uno.

Eso es exactamente lo que le pasó a Boletia con el SDK de su proveedor de pago. Vamos a verlo.

Ejemplo trabajado: el SDK que nadie envolvió

Hay un dato de Boletia que en el módulo 1 no mencionamos porque no hacía falta todavía. Después de operar dos años solo en México con tarjeta y efectivo, el año pasado la plataforma empezó a vender boletos en Colombia, Chile y Perú. Ninguno de los proveedores que ya usaba cubría transferencias bancarias ni billeteras digitales en esos países, así que el equipo integró un cuarto: Zafiro, una pasarela regional. Zafiro cubre lo que hacía falta, cobra comisiones razonables y funciona.

Su SDK de Python, en cambio, es un desastre. Y no un desastre exótico: es exactamente el tipo de desastre que te vas a encontrar en el mundo real, porque nació de un SDK de Java traducido a medias y después parcheado por gente distinta durante cuatro años.

Así se ve, resumido a lo que Boletia usa de él:

# Paquete: zafiropay 2.4.1  — el SDK oficial del proveedor. NO es código de Boletia.
# Está aquí solo para que veas contra qué peleamos. No se puede modificar.

import zafiropay

# ── 1. Configuración global obligatoria ────────────────────────────────────
# Hay que llamar a esto UNA vez, en algún lado, antes de que cualquier cliente
# funcione. Si se olvida, el error aparece mucho después y no dice dónde faltó.
zafiropay.configure(apiKey="...", env="live")     # apiKey en camelCase. Es global.

# ── 2. El cliente ──────────────────────────────────────────────────────────
# No recibe la credencial: la lee del estado global de arriba.
client = zafiropay.ZafiroClient()

# ── 3. Cobrar ──────────────────────────────────────────────────────────────
# El monto va como TEXTO con dos decimales. No es un número.
res = client.doCharge(
    amount="1250.00",
    currency="MXN",
    ref="BOL-8891",
    idem_key="orden-8891-intento-1",   # se ignora en silencio si mide más de 32 caracteres
)

# ── 4. Reembolsar ──────────────────────────────────────────────────────────
# El monto va en CENTAVOS ENTEROS. Sí: distinta unidad que al cobrar.
client.refund_payment(transaction="zf_01HX...", cents=125000)

# ── 5. Consultar el estado ─────────────────────────────────────────────────
# Tercera convención de nombre, en el mismo objeto.
client.Status(txn_id="zf_01HX...")

Tres métodos, tres convenciones de nombre distintas —doCharge en camelCase, refund_payment en snake_case, Status en PascalCase—, dos unidades distintas para el dinero y una credencial que vive en una variable global. Y todavía no llegamos a lo interesante.

Lo interesante son las respuestas. doCharge devuelve un diccionario, y ese diccionario no tiene siempre las mismas llaves:

# Pago con tarjeta aprobado al instante:
{
    "ok": True,
    "id": "zf_01HXQ8...",              # ← se llama "id"
    "status": "APPROVED",              # ← en MAYÚSCULAS
    "authorization_code": "A19X2",
    "amount": "1250.00",
}

# Transferencia bancaria (queda pendiente hasta que el banco confirme):
{
    "ok": True,
    "transaction_id": "zf_01HXQ9...",  # ← ahora se llama "transaction_id". No "id".
    "status": "pending",               # ← ahora en minúsculas
    "instructions": {"bank": "Bancolombia", "account": "...", "expires_at": "..."},
    # ← no hay "authorization_code": todavía no hay autorización que dar
}

# Rechazado por el banco:
{
    "ok": False,                       # ← NO lanza excepción. Devuelve ok=False.
    "status": "DECLINED",
    "reason_code": 51,                 # ← un entero, sin texto que lo explique
}

# Y a veces, para el mismo rechazo, lanza:
#   zafiropay.ZafiroError(code="insufficient_funds", message="...")
# donde .code unas veces es texto y otras es un entero, y donde el texto
# está en .message o en .detail según qué parte del SDK lo haya lanzado.

# Y si se cae la red, lanza directamente el error de la librería HTTP que usa
# por dentro:  requests.exceptions.ConnectionError

Junta todo y tienes el retrato completo del vecino desprolijo:

Lo que hace el SDKPor qué duele
Tres convenciones de nombre en el mismo objetoNadie recuerda cuál toca; hay que abrir la documentación cada vez
Monto como texto al cobrar, en centavos enteros al reembolsarUn error de unidad aquí cobra cien veces de más. Ha pasado en la industria
La llave del identificador cambia según el medio de pago (id / transaction_id)Todo el que lee la respuesta escribe su propio "si no está uno, usa el otro"
Seis valores de status para tres estados realesCada archivo normaliza a su manera, y ninguno igual
A veces devuelve el fallo, a veces lo lanzaQuien llama necesita try/except y revisar ok. Casi nadie hace las dos
Filtra el error de su librería HTTP internaTu código termina importando requests para atrapar un error de pagos
Credencial en estado globalNo puedes tener dos configuraciones a la vez, y las pruebas se pisan entre sí
import zafiropay consulta la versión en su servidorTus pruebas necesitan internet. Y si su servidor está lento, tu suite tarda

Esa última fila merece un momento. El SDK, al importarse, hace una llamada de red para avisarte si hay una versión nueva. Se puede apagar con la variable de entorno ZAFIRO_OFFLINE=1, cosa que está documentada en un párrafo del final de su guía. Alguien en Boletia lo descubrió después de tres semanas de pruebas que fallaban los martes.

Dónde está el desorden hoy. Esta es la parte que quiero que veas bien, porque es el diagnóstico del módulo. El SDK no está mal usado en un archivo: está usado en cinco, y cada uno resolvió los mismos problemas por su cuenta.

boletia/
├── app.py                     ← llama a zafiropay.configure(...) al arrancar
├── checkout/checkout.py       ← llama a client.doCharge(...) y normaliza el status
├── api/routes.py              ← lee res["id"] or res["transaction_id"] para la respuesta HTTP
├── admin/refunds.py           ← llama a client.refund_payment(cents=...) y normaliza el status OTRA VEZ
├── jobs/reconcile.py          ← llama a client.Status(...) de madrugada y normaliza el status UNA TERCERA VEZ
└── tests/conftest.py          ← pone ZAFIRO_OFFLINE=1 para que la suite no dependa de su servidor

Y ahora mira las tres normalizaciones del estado, que están en tres archivos distintos y no son iguales:

# checkout/checkout.py
if res.get("status", "").upper().startswith("APPROV"):
    order.status = "paid"
elif res.get("status", "").lower() == "pending":
    order.status = "pending"
else:
    order.status = "failed"
# admin/refunds.py
status = (res.get("status") or "").lower()
if status in ("approved", "refunded"):
    refund.status = "done"
else:
    refund.status = "failed"
    # Ojo: "pending_review" cae aquí y se marca como fallido. No lo es.
# jobs/reconcile.py
raw = res["status"]
if raw in ("APPROVED", "approved"):
    mark_paid(order_id)
elif raw in ("PENDING_REVIEW", "pending"):
    pass          # se revisa mañana
elif raw in ("DECLINED", "rejected"):
    mark_failed(order_id)
# ← si llega un valor nuevo, este job no hace nada y nadie se entera

Qué esperar de este diagnóstico. Vamos por lo evidente primero y por lo importante después.

Lo evidente: hay tres copias de la misma decisión y las tres se comportan distinto. admin/refunds.py trata "pending_review" como fallo, cuando en realidad significa que el proveedor está revisando la operación. Eso es un error real, con consecuencias reales —un reembolso que sí va a ocurrir se muestra como fallido, alguien lo reintenta y el cliente recibe el dinero dos veces—. Y no es que alguien haya sido descuidado: cada uno de los tres escribió su normalización mirando los casos que su archivo necesitaba, en semanas distintas, sin saber que los otros dos existían.

Lo importante, que es más difícil de ver: el problema no es la calidad del SDK, es la ausencia de una frontera. Aunque el SDK fuera impecable, tener cinco archivos que lo llamen directamente ya sería un problema, porque el día que Zafiro publique la versión 3 y cambie una firma, tienes cinco lugares que revisar. Un SDK desprolijo hace el dolor más visible y más urgente, pero la causa es estructural: Boletia dejó que un detalle externo se convirtiera en conocimiento repartido por todo el sistema.

Y hay un tercer efecto, silencioso, que es el que más cuesta caro con el tiempo. Fíjate en el conftest.py con su ZAFIRO_OFFLINE=1. Ese archivo existe porque las pruebas de Boletia —todas, incluidas las que no tienen nada que ver con pagos— cargan indirectamente el SDK y quedan atadas a un servidor de un tercero. Cuando una dependencia externa se filtra por todo el sistema, no contamina solo el código de producción: contamina la manera de probarlo. Y una suite de pruebas que necesita internet es una suite que la gente empieza a saltarse.

Ese es el estado de las cosas. El resto del módulo lo arregla.

Los cuatro patrones de la familia, en una línea cada uno

Los cuatro patrones de este módulo se parecen tanto en su forma —todos consisten en un objeto que contiene a otro— que es fácil confundirlos. La forma es casi la misma; la intención es distinta, y la intención es lo que hay que aprender. Aquí está la tabla que conviene que te lleves:

PatrónIntención en una fraseLa pregunta que responde
AdapterTraducir una interfaz a otra"Esto no encaja con lo que mi código espera"
FacadeSimplificar el acceso a algo complicado"Esto tiene diez pasos y yo solo quiero uno"
DecoratorAgregar comportamiento manteniendo la interfaz"Quiero que esto además haga X, sin modificarlo"
CompositeTratar igual a una cosa y a un grupo de cosas"Esto puede ser uno o varios y no quiero preguntar cuál"

Vale la pena detenerse en las diferencias que más se confunden, porque van a aparecer en revisiones de código y saber contestarlas en una frase es media victoria.

Adapter contra Facade. Los dos ponen un objeto tuyo delante de algo ajeno, y por eso se confunden todo el tiempo. La diferencia: Adapter traduce, Facade simplifica. Un Adapter existe porque hay una interfaz que tú necesitas y otra que te dan, y no coinciden; si coincidieran, el Adapter sobraría. Un Facade existe porque hay muchos pasos y quien llama solo quiere uno; los pasos podrían estar perfectamente diseñados y el Facade seguiría valiendo la pena. Dicho de otra forma: el Adapter cambia la forma, el Facade cambia la cantidad.

Decorator contra Adapter. Los dos envuelven un objeto. La diferencia es qué sale del otro lado: el Decorator conserva la misma interfaz que el objeto que envuelve —entra un PaymentProvider, sale un PaymentProvider— mientras que el Adapter la cambia a propósito. Esa propiedad es la que permite apilar decoradores uno sobre otro, cosa que con adaptadores no tiene sentido.

Composite contra los otros tres. Este es el raro de la familia y el que menos vas a usar. Los tres primeros resuelven el contacto con lo externo; Composite no. Composite resuelve un problema de forma de los datos: cuando algo puede ser una hoja o una rama y quieres tratarlas igual. Está en este módulo porque comparte la mecánica —un objeto que contiene otros— y porque, honestamente, es el que más se fuerza donde no corresponde. La lección 6 le dedica la mitad del tiempo a decir cuándo no usarlo.

Y hay una quinta cosa, que no es un patrón, que va a estar presente todo el módulo. Se llama escribe una función. Es lo que la lección 7 defiende, y en más casos de los que a nadie le gusta admitir, gana.

Lo que cambia cuando pones una frontera

Antes de entrar a los patrones concretos vale la pena ser preciso sobre qué se gana, porque "aislar la dependencia" es de esas frases que se repiten sin definirse. Cuando el SDK de Zafiro quede detrás de una interfaz propia —y eso ocurre en la lección 3— van a cambiar cuatro cosas concretas y medibles.

El desorden queda contenido. Hoy hay cinco archivos que saben que el monto va como texto al cobrar y en centavos al reembolsar. Después habrá uno. Ese archivo va a seguir siendo feo por dentro, y está bien que lo sea: su trabajo es absorber la fealdad para que nadie más la vea. Un buen adaptador es un archivo desagradable rodeado de archivos limpios, y eso es una mejora enorme respecto de cinco archivos moderadamente sucios.

Las pruebas dejan de necesitar red. Cuando el resto del sistema depende de una interfaz propia —PaymentProvider con charge y refund— cualquier prueba puede pasarle una implementación falsa de diez líneas. No hace falta simular el SDK, ni parchear módulos, ni la variable ZAFIRO_OFFLINE. Esto es tan valioso que en muchos equipos es la razón principal para escribir el adaptador, por encima de la limpieza.

Cambiar de proveedor se vuelve local. Si el año que viene Zafiro sube sus comisiones y Boletia se muda a otra pasarela, el trabajo es escribir un archivo nuevo que cumpla la misma interfaz y cambiar una línea en la factory del módulo 4. El checkout, las rutas, el panel de reembolsos y el job de conciliación no se enteran. Compáralo con hoy: cinco archivos, cada uno con su propia normalización, y ninguna prueba que garantice que quedaron equivalentes.

Aparece un lugar donde poner cosas. Este es el beneficio que menos se menciona y el que más se agradece. Cuando todas las llamadas al proveedor pasan por un punto, ese punto es el lugar natural para agregar reintentos, un registro de auditoría, una métrica de latencia o una alerta. Sin ese punto, cada uno de esos agregados vuelve a ser una modificación en cinco archivos. La lección 5 —Decorator— vive entera de esta propiedad.

Ahora el costo, porque en esta guía ningún patrón se presenta sin él. Poner una frontera cuesta:

  • Un archivo más y un salto más al leer. Quien quiera saber qué le llega de verdad al proveedor tiene que abrir el adaptador. Con una dependencia es trivial; con quince fronteras anidadas es el infierno de indirección del módulo 2.
  • Riesgo de una abstracción que miente. Si diseñas la interfaz mirando solo a Zafiro, va a tener la forma de Zafiro con otros nombres, y el día que entre otro proveedor no va a encajar. Es el error más caro de este módulo y la lección 3 le dedica una sección.
  • La tentación de envolverlo todo. Después de escribir un buen adaptador, la reacción natural es querer uno para cada dependencia: para la librería de fechas, para la de códigos QR, para la que genera PDFs. La mayoría no lo necesita. Eso es la lección 7.

Errores comunes

Diseñar la interfaz propia mirando el SDK (de criterio). Qué pasa: alguien se sienta a escribir el adaptador con la documentación del proveedor abierta al lado, y va traduciendo método por método: doCharge se vuelve charge, refund_payment se vuelve refund, Status se vuelve get_status. El resultado se ve limpio y no lo es: es el SDK de Zafiro con nombres en snake_case. Sigue teniendo los mismos conceptos, los mismos parámetros y las mismas rarezas, solo que ahora con una capa más que atravesar. Por qué pasa: es lo que la mano quiere hacer. El SDK está ahí, es concreto, tiene una lista de métodos; la interfaz propia hay que inventarla, y eso exige preguntarse qué necesita Boletia, que es una pregunta más difícil. Cómo detectarlo: cuenta los métodos de tu interfaz y los del SDK. Si son los mismos y en el mismo orden, no diseñaste nada. Otra señal: si tu interfaz tiene un parámetro llamado idem_key, un concepto que solo existe porque Zafiro lo llama así, la frontera está filtrando. Cómo corregirlo: escribe primero la interfaz que te gustaría tener, sin mirar el SDK. Después revisa si el SDK puede cumplirla. Casi siempre puede, y las dos o tres cosas que no pueda te van a enseñar algo real sobre el proveedor. La lección 3 hace exactamente eso, en ese orden.

Confundir "el SDK es feo" con "hace falta un patrón" (de criterio). Qué pasa: alguien encuentra una librería con nombres inconsistentes y su reacción inmediata es envolverla. A veces está bien; muchas veces no. Si la librería se usa en un solo lugar, si su interfaz es estable y si no necesitas sustituirla en pruebas, envolverla te deja con un archivo más y cero ventajas. Por qué pasa: la fealdad produce una incomodidad genuina, y envolverla se siente como limpiar. Pero la incomodidad estética no es un criterio de diseño; la que importa es la pregunta de cuántos lugares tocarías si esa librería cambiara. Cómo detectarlo: busca todos los usos de la dependencia en el proyecto. Si hay uno, la respuesta casi seguro es dejarlo así o meterlo en una función. Cómo corregirlo: las tres preguntas de la lección 7, aplicadas en frío. En el caso de Zafiro las tres dan "sí" —cinco lugares, pruebas que necesitan red, un proveedor que ya cambió su API una vez— y por eso ese caso sí justifica el trabajo. No todos lo hacen.

Creer que la frontera protege de todo (conceptual). Qué pasa: alguien escribe el adaptador, respira aliviado y da el problema por resuelto. Meses después, el proveedor cambia el significado de un estado —lo que antes era "pending" ahora se divide en dos casos— y resulta que el adaptador traducía las palabras pero no protegía del cambio de concepto. O peor: el proveedor deja de soportar reembolsos parciales, y no hay patrón que compense eso. Por qué pasa: se confunden dos cosas distintas. Un Adapter aísla de la forma de la dependencia —sus nombres, sus unidades, sus tipos de error—, no de su semántica ni de sus limitaciones. Es el adaptador de enchufe que no convierte el voltaje. Cómo detectarlo: pregúntate qué pasaría si el proveedor cambiara el significado de un campo sin cambiar su nombre. Si tu respuesta es "el adaptador me protege", te estás confiando de más. Cómo corregirlo: sé explícito sobre qué cubre la frontera. Documenta en el adaptador los supuestos semánticos que estás haciendo —"asumimos que PENDING_REVIEW termina siempre en aprobado o rechazado en menos de 72 horas"— para que el día que dejen de ser ciertos haya un lugar donde se note. Y prueba el adaptador contra respuestas reales guardadas, no solo contra las que te inventaste.

Ejercicios

Ejercicio 1 — Encuentra las tres normalizaciones y di qué las diferencia. Vuelve a los tres fragmentos de checkout.py, refunds.py y reconcile.py. Para cada uno responde: (a) qué valores de status maneja correctamente, (b) qué valor maneja mal o no maneja, y (c) qué consecuencia concreta tiene ese error para un cliente de Boletia. Después decide cuál de los tres es el más peligroso y justifica.

Ver solución

(a) y (b), archivo por archivo:

checkout/checkout.py maneja bien "APPROVED" y "approved" —el .upper().startswith("APPROV") los cubre a los dos— y maneja bien "pending". No maneja "PENDING_REVIEW": al pasar por .lower(), "pending_review" no es igual a "pending", así que cae en el else y la orden se marca como failed. Consecuencia: una compra que el proveedor está revisando y que probablemente se apruebe se muestra al cliente como fallida. El cliente vuelve a comprar, y si la primera termina aprobándose, pagó dos veces.

admin/refunds.py maneja "approved" y "refunded". No maneja "pending_review" ni ningún valor en mayúsculas más allá del .lower() inicial —eso sí lo cubre—. El problema es el mismo pending_review, con una consecuencia peor: un reembolso en revisión aparece como fallido en el panel, un operador lo reintenta, y el cliente recibe el dinero dos veces. Boletia pierde ese dinero.

jobs/reconcile.py es el más completo: cubre los seis valores conocidos. Su falla es distinta y más silenciosa: si Zafiro agrega un estado nuevo —digamos "CHARGEBACK"—, el job no entra en ninguna rama, no hace nada y no avisa. La orden se queda para siempre en un limbo que nadie revisa.

(c) El más peligroso es admin/refunds.py, porque su error cuesta dinero de forma directa y difícil de recuperar. Pero hay una respuesta más interesante y también correcta: el más peligroso a largo plazo es reconcile.py, porque falla en silencio. Un error visible se corrige; un error que no se ve se acumula. Las dos respuestas son defendibles siempre que la justificación distinga entre gravedad inmediata y detectabilidad.

Por qué funciona: acabas de hacer el ejercicio que justifica el módulo entero. Ninguno de los tres autores fue descuidado; los tres escribieron una normalización razonable para el caso que tenían delante. El problema no es la calidad de cada fragmento, es que la misma decisión esté tomada tres veces. Cuando en la lección 3 exista un solo lugar que traduzca el estado, estos tres errores dejan de ser posibles —no porque alguien sea más cuidadoso, sino porque solo hay un lugar donde equivocarse—.

Ejercicio 2 — Clasifica cinco situaciones según el patrón que piden. Para cada una, di si el problema se parece más a un Adapter, un Facade, un Decorator, un Composite o a ninguno de los cuatro, y justifica en una línea usando la intención, no la forma.

(a) El equipo quiere medir cuánto tarda cada llamada al proveedor de pago, sin modificar las clases de los proveedores. (b) Publicar un evento en Boletia hoy exige llamar a siete funciones en orden: validar, crear el registro, generar los boletos, calcular precios, subir la imagen, indexar la búsqueda y avisar al organizador. El panel de administración solo quiere "publicar". (c) La librería de códigos QR que usa Boletia devuelve un objeto propio con un método .render_to(path), pero el generador de boletos necesita los bytes de la imagen para meterlos en el PDF. (d) Un evento puede tener un descuento simple ("10% para estudiantes") o una combinación ("10% para estudiantes y 200 pesos menos por comprar más de cuatro boletos"), y el calculador de precios no quiere preguntar cuál de los dos le tocó. (e) Boletia usa una librería para dar formato a fechas en zonas horarias. Se usa en un solo archivo, la librería lleva ocho años sin cambiar su API y en las pruebas se usa la de verdad sin problema.

Ver solución

(a) Decorator. La clave está en "sin modificar las clases": quieres agregar comportamiento —medir— conservando la interfaz, para que el resto del sistema siga usando un PaymentProvider cualquiera. Si tuvieras que cambiar la interfaz, ya no sería Decorator.

(b) Facade. Siete pasos, un solo deseo. Fíjate en que los siete pasos podrían estar perfectamente escritos: el problema no es que estén mal, es que son muchos para quien llama. Eso es exactamente la intención de Facade.

(c) Adapter. Hay una interfaz que necesitas —"dame los bytes"— y otra que te dan —"escribo en un archivo"—, y no coinciden. Traducir esa diferencia es el trabajo del Adapter. (Aunque ojo: puede que aquí la respuesta correcta sea una función de tres líneas. Guarda la duda para la lección 7.)

(d) Composite. Un descuento puede ser una hoja o un grupo, y quien lo usa quiere tratarlos igual. Este es el caso legítimo del patrón y es el que trabajaremos en la lección 6.

(e) Ninguno. Un solo lugar de uso, API estable, sin necesidad de sustituirla en pruebas: las tres preguntas de la lección 7 dan "no". Envolver esto sería agregar un archivo y un salto a cambio de nada. Este caso está en el ejercicio a propósito, porque en un módulo sobre envolver cosas la respuesta "no envuelvas" tiene que estar sobre la mesa desde el primer día.

Por qué funciona: los cuatro patrones tienen casi la misma forma en el diagrama —una caja que contiene otra— y por eso se confunden. Distinguirlos por intención es la única manera que funciona, y es también la manera en que se usan en una conversación real: cuando alguien dice "esto pide un Facade", está describiendo un problema, no un diagrama.

Ejercicio 3 — Predice el costo de un cambio. Zafiro anuncia su versión 3.0. Entre otros cambios: doCharge pasa a llamarse create_payment, el monto pasa a ser un entero en centavos (como el reembolso), y la respuesta unifica el identificador en la llave "id" para todos los medios de pago. Con Boletia tal como está hoy, enumera exactamente qué archivos hay que abrir y qué hay que revisar en cada uno. Después estima lo mismo suponiendo que existiera un adaptador. No escribas código: escribe las dos listas.

Ver solución

Hoy, sin adaptador. Hay que abrir al menos cinco archivos:

  1. app.py — revisar si configure() cambió de firma. Probablemente sí, si el SDK cambió de versión mayor.
  2. checkout/checkout.py — cambiar el nombre del método, cambiar el formato del monto de texto a centavos enteros, y revisar la normalización del estado por si los valores cambiaron.
  3. api/routes.py — quitar el res["id"] or res["transaction_id"], que ahora sobra. Fácil de olvidar, porque seguiría funcionando: el or con una llave que siempre existe simplemente nunca usa la segunda parte. Un cambio que "sigue funcionando" es el que se queda ahí diez años.
  4. admin/refunds.py — revisar si refund_payment también cambió, y revisar la segunda normalización del estado.
  5. jobs/reconcile.py — revisar si Status cambió y la tercera normalización.

Más tests/conftest.py por si la variable de entorno cambió de nombre. Y el detalle que hace este ejercicio incómodo: no hay ninguna prueba que garantice que los cinco quedaron equivalentes, porque las pruebas que existen llaman al SDK de verdad. Para verificar el cambio hace falta un entorno de sandbox del proveedor y probar a mano cada camino.

El riesgo real no es el trabajo —son unas horas—: es que la conversión del monto se haga bien en cuatro archivos y mal en el quinto. Multiplicar por cien un cobro es un error de una línea.

Con un adaptador. Un archivo: payments/zafiro_provider.py. Se cambian ahí dentro el nombre del método, la conversión del monto y la lectura del identificador. Todo lo demás del sistema no se entera, porque nadie más conoce esos detalles.

Y aparece una ventaja que no es obvia: como el resto del sistema depende de la interfaz propia, las pruebas que ya existen —con un PaymentProvider falso— siguen valiendo y no hay que tocarlas. La verificación se concentra en las pruebas del adaptador, que se pueden escribir contra respuestas reales guardadas del proveedor. Pasas de "probar todo el sistema a mano contra un sandbox" a "probar un archivo con datos guardados".

Por qué funciona: este ejercicio convierte la palabra "acoplamiento" en un número que puedes decir en una reunión. No es "el código está acoplado al proveedor"; es "un cambio de versión mayor toca cinco archivos y no tenemos cómo verificarlo automáticamente". La segunda frase mueve prioridades; la primera no. Es, además, exactamente la forma en que vas a tener que justificar este trabajo cuando alguien pregunte por qué vale la pena.

Resumen y siguiente paso

En esta lección viste el problema que une a esta familia de patrones: el contacto con lo que no controlas. Los módulos 3 y 4 trabajaron sobre código tuyo, donde si algo no encaja lo cambias. Este trabaja sobre el código ajeno —la librería del tercero, el SDK del proveedor, el módulo viejo— que tiene sus propias ideas y no te consultó.

Conociste al villano del módulo con detalle: el SDK zafiropay, con sus tres convenciones de nombre en el mismo objeto, su monto como texto al cobrar y en centavos al reembolsar, su identificador que se llama id o transaction_id según el medio de pago, sus seis valores de estado para tres estados reales, su costumbre de a veces devolver el fallo y a veces lanzarlo, su credencial en una variable global y su consulta de versión al importarse que ata las pruebas de Boletia a un servidor ajeno. Y viste dónde está su desorden hoy: cinco archivos, tres normalizaciones distintas del estado, y un error real —pending_review tratado como fallo— que le puede costar dinero a la empresa.

Viste los cuatro patrones del módulo distinguidos por intención, que es la única forma que funciona: Adapter traduce, Facade simplifica, Decorator agrega manteniendo la interfaz, Composite trata igual a uno y a varios. Y viste qué cambia concretamente cuando pones una frontera: el desorden queda contenido en un archivo, las pruebas dejan de necesitar red, cambiar de proveedor se vuelve local y aparece un punto único donde agregar cosas.

Antes de avanzar deberías poder: explicar en una frase el problema común de esta familia; nombrar tres rarezas concretas del SDK de Zafiro; decir la diferencia entre Adapter y Facade sin dudar; y estimar, en número de archivos, el costo de un cambio de versión con y sin frontera.

La lección 2 desarma el primero de los cuatro. Vamos a definir Adapter con precisión, a separar sus piezas una por una, y a responder por qué —de los veintitrés del catálogo— es probablemente el que mejor relación valor/costo tiene en código real. Trabajaremos sobre un caso pequeño y distinto al de los pagos, para que la idea quede limpia; el caso grande de Zafiro es la lección 3, y para entonces vas a tener la herramienta lista.

Recursos

  • Refactoring Guru — Structural Patterns — la familia completa con sus diagramas. Útil para ver de un vistazo por qué las formas se parecen tanto entre sí.
  • Anti-Corruption Layer (Eric Evans, Domain-Driven Design) — el mismo problema de este módulo, con otro nombre y a mayor escala. Vale la pena conocer el término: "capa anticorrupción" es lo que muchos equipos dicen cuando quieren decir "adaptador con opinión".
  • python-patterns.guide — The Adapter Pattern (sección de «Composition Over Inheritance») — Brandon Rhodes sobre cómo se ve este patrón en Python, con la comparación honesta contra las alternativas más simples.
  • The Grug Brained Developer — sobre por qué la complejidad es el enemigo. Léelo antes de la lección 7; es el mejor antídoto contra la tentación de envolverlo todo.