Módulo 1: Qué son de verdad los patrones
8. Proyecto: nombra los patrones de Boletia
Descripción
Al terminar este proyecto vas a tener un inventario de estructuras de Boletia: un documento donde, rincón por rincón, dices qué estructura hay, qué problema resuelve ahí, cómo se llama si tiene nombre, y —parte obligatoria de la entrega— cuáles no lograste nombrar. Sin cambiar una sola línea de código. Ese documento no es un ejercicio de fin de módulo que se archiva: es tu material de trabajo para los siete módulos restantes. En el módulo 2 vas a tomar una de sus entradas y quitarla. En los módulos 3 a 6 vas a volver a él para implementar. En el módulo 8 lo vas a usar como punto de partida del capstone.
Esto importa porque es exactamente lo que se hace en las primeras semanas de un trabajo nuevo, y casi nadie lo practica antes de tener que hacerlo en serio. Te dan acceso a un repositorio que no escribiste, con años encima y decisiones que nadie recuerda, y tu valor durante ese primer mes no está en escribir código: está en entender la estructura lo suficiente para poder opinar. La persona que entrega un mapa de las estructuras de un sistema en su segunda semana se vuelve útil mucho antes que la que se lanza a escribir.
Y hay una razón pedagógica para que el proyecto sea de solo lectura. La lección 7 acaba de advertirte sobre el martillo nuevo. La forma más segura de consolidar la habilidad de nombrar sin caer en la de aplicar es prohibir explícitamente aplicar. Este proyecto es un músculo aislado a propósito: reconocer, formular el problema, nombrar cuando se puede y admitir cuando no.
Conexión con el módulo: este proyecto junta las siete lecciones anteriores. Usa la definición de tres casillas de la lección 2 —cada entrada del inventario es un problema, una forma y unas consecuencias—, el vocabulario de la lección 3, la perspectiva de la lección 4 —¿esta estructura llegó antes o después del problema?—, la técnica de cuatro pasos de la lección 5, el mapa de los ocho de la lección 6, y el freno de la lección 7 para no etiquetar todo lo que se mueve.
El encargo, tal como llega en la vida real
Es tu segunda semana en Boletia. Tu líder técnica te escribe:
"Antes de que te asignemos algo, quiero que hagas un recorrido. Necesito que leas el sistema y me digas qué ves: dónde hay estructura de verdad, dónde hay estructura que sobra, y qué partes no entiendes. No cambies nada. Lo que quiero es tu lectura, no tu opinión sobre cómo debería ser. La gente que lleva años aquí ya no puede ver el código con ojos nuevos; tú sí, por dos semanas más."
Es un encargo real y es un regalo. Fíjate en tres cosas de cómo está formulado.
"No cambies nada." Esto no es desconfianza: es que el valor del ejercicio está en la lectura. En cuanto empiezas a cambiar, dejas de leer y empiezas a defender lo que cambiaste.
"Qué partes no entiendes." Está pidiendo explícitamente el hueco. En un equipo sano, "no entiendo este rincón" es información sobre el código, no sobre ti. Si tres personas nuevas seguidas no entienden el mismo rincón, el rincón tiene un problema.
"Tu lectura, no tu opinión sobre cómo debería ser." Es la separación entre describir y juzgar. Este proyecto es de descripción. El juicio empieza en el módulo 2.
El código de Boletia
Esto es el sistema, condensado a lo esencial. Los archivos están completos en lo que importa para el inventario; lo que se omite está marcado. Léelo con la técnica de la lección 5: primero la forma de los archivos, después las pistas sin nombrarlas, después el hilo de una compra, y al final los nombres.
# ============================================================
# api/routes.py — la entrada HTTP
# ============================================================
def post_checkout(request):
body = request.json
# La Order se arma aquí, campo por campo, antes de mandarla a cobrar.
order = Order(
id=next_order_id(),
customer_id=body["customer_id"],
ticket_ids=body["ticket_ids"],
total=0.0, # se calcula después, en el checkout
provider=body["provider"], # "stripe" | "mercadopago" | "cash"
status="pending",
created_at=now(),
)
# Validaciones sueltas, aquí mismo.
if not order.ticket_ids:
return error(400, "La orden no tiene boletos")
if order.provider not in ("stripe", "mercadopago", "cash"):
return error(400, "Proveedor inválido")
if body.get("coupon") and order.provider == "cash":
return error(400, "Los cupones no aplican en pago en efectivo")
result = checkout(order, coupon=body.get("coupon"))
return json_response(serialize_order(result))
def get_report(request, event_id):
fmt = request.query.get("format", "csv")
path = ReportManager(EXPORTERS).export(event_id, fmt)
return file_response(path)
# ============================================================
# checkout/checkout.py — el orquestador. ~300 líneas en el original.
# ============================================================
def checkout(order, coupon=None):
tickets = [repository.get_ticket(tid) for tid in order.ticket_ids]
# ---- 1. Precio -------------------------------------------------
subtotal = 0.0
for ticket in tickets:
subtotal += calculate_price(ticket, order.created_at)
if coupon:
# Un solo tipo de cupón hasta hoy: porcentaje fijo sobre el subtotal.
subtotal = subtotal * (1 - COUPON_RATES[coupon])
fee = subtotal * SERVICE_FEE_RATE
order.total = round(subtotal + fee, 2)
# ---- 2. Asientos ------------------------------------------------
event = repository.get_event(tickets[0].event_id)
if event.has_numbered_seats:
plugin = SeatingPluginRegistry.get(settings.SEATING_PLUGIN)
for ticket in tickets:
ticket.seat = plugin.assign(event, order).code
# ---- 3. Cobro ---------------------------------------------------
if order.provider == "stripe":
provider = StripeProvider(StripeClient(api_key=settings.STRIPE_KEY))
elif order.provider == "mercadopago":
provider = MercadoPagoProvider(MercadoPagoClient(token=settings.MP_TOKEN))
elif order.provider == "cash":
provider = CashProvider()
else:
raise ValueError(f"Proveedor desconocido: {order.provider}")
result = provider.charge(order)
if not result.ok:
order.status = "cancelled"
repository.save_order(order)
raise PaymentFailed(order.id, result.reference)
order.status = "paid"
repository.save_order(order)
# ---- 4. Avisos ---------------------------------------------------
customer = repository.get_customer(order.customer_id)
email_channel.send(customer.email, build_confirmation(order))
if customer.phone:
sms_channel.send(customer.phone, build_short_confirmation(order))
if customer.push_token:
push_channel.send(customer.push_token, build_push_confirmation(order))
organizer = repository.get_customer(event.organizer_id)
email_channel.send(organizer.email, build_organizer_alert(order, event))
analytics.track("order_paid", order_id=order.id, total=order.total)
return order
# ============================================================
# pricing/calculator.py
# ============================================================
def calculate_price(ticket, order_date):
if ticket.kind == "general":
return ticket.base_price
elif ticket.kind == "vip":
return ticket.base_price * 1.40
elif ticket.kind == "early_bird":
cutoff = get_early_bird_cutoff(ticket.event_id)
return ticket.base_price * 0.75 if order_date < cutoff else ticket.base_price
elif ticket.kind == "courtesy":
if courtesy_count(ticket.event_id) > COURTESY_LIMIT:
raise CourtesyLimitExceeded(ticket.event_id)
return 0.0
else:
raise ValueError(f"Tipo de boleto desconocido: {ticket.kind}")
# En otros dos archivos, la misma pregunta contestada otra vez:
# tickets/transfer.py → if ticket.kind in ("courtesy",): raise NotTransferable
# refunds/policy.py → if ticket.kind == "early_bird": window = 3 else: window = 14
# ============================================================
# payments/ — la forma común y los tres proveedores
# ============================================================
class PaymentProvider: # payments/provider.py
def charge(self, order): raise NotImplementedError
def refund(self, order): raise NotImplementedError
class StripeProvider(PaymentProvider): # payments/stripe_provider.py
def __init__(self, client):
self.client = client
def charge(self, order):
# El SDK habla en centavos y devuelve su propio diccionario.
r = self.client.create_charge(amount=int(order.total * 100), currency="MXN")
return ChargeResult(ok=r["status"] == "succeeded", reference=r["id"])
def refund(self, order):
return self.client.create_refund(charge_id=order.external_ref)
class MercadoPagoProvider(PaymentProvider): # payments/mercadopago_provider.py
def __init__(self, client):
self.client = client
def charge(self, order):
# Este SDK quiere float y un concepto de texto.
r = self.client.pay(order.total, description=f"Boletia #{order.id}")
return ChargeResult(ok=r["approved"], reference=r["payment_id"])
def refund(self, order):
return self.client.refund(payment_id=order.external_ref)
class CashProvider(PaymentProvider): # payments/cash_provider.py
def charge(self, order):
# No cobra: genera una referencia para pagar en tienda.
return ChargeResult(ok=True, reference=generate_cash_reference(order.id))
def refund(self, order):
# Los pagos en efectivo se devuelven a mano, en ventanilla.
raise NotImplementedError("Reembolso manual")
# ============================================================
# notifications/ — visto en la lección 5, resumido aquí
# ============================================================
# channel.py → NotificationChannel: send() + is_available_for()
# email/sms/push → tres implementaciones
# retrying.py → RetryingChannel: envuelve otro canal y reintenta
# notifier.py → CHANNELS = [...] y notify(customer, message)
# manager.py → NotificationManager: 6 métodos sin relación entre sí
#
# Nota importante: notify() existe, pero checkout() NO lo usa.
# checkout llama a los canales directo (ver arriba). notify() lo usa solo
# el script de recordatorios de eventos próximos.
# ============================================================
# reports/ — tres exportadores
# ============================================================
class CsvExporter: # reports/csv_exporter.py
def export(self, event_id):
rows = db.fetch_attendees(event_id)
rows = sorted(rows, key=lambda r: r["name"])
body = ",".join(HEADERS) + "\n"
for r in rows:
body += ",".join(str(r[h]) for h in HEADERS) + "\n"
path = f"/tmp/attendees_{event_id}.csv"
write_text(path, body)
return path
class PdfExporter: # reports/pdf_exporter.py
def export(self, event_id):
rows = db.fetch_attendees(event_id)
rows = sorted(rows, key=lambda r: r["name"])
doc = PdfDocument(title=f"Asistentes evento {event_id}")
for r in rows:
doc.add_row([str(r[h]) for h in HEADERS])
path = f"/tmp/attendees_{event_id}.pdf"
doc.save(path)
return path
class XlsxExporter: # reports/xlsx_exporter.py
def export(self, event_id):
rows = db.fetch_attendees(event_id)
rows = sorted(rows, key=lambda r: r["name"])
book = Workbook(); sheet = book.active
sheet.append(HEADERS)
for r in rows:
sheet.append([r[h] for h in HEADERS])
path = f"/tmp/attendees_{event_id}.xlsx"
book.save(path)
return path
EXPORTERS = {"csv": CsvExporter(), "pdf": PdfExporter(), "xlsx": XlsxExporter()}
class ReportManager: # reports/manager.py
def __init__(self, exporters):
self.exporters = exporters
def export(self, event_id, fmt):
if fmt not in self.exporters:
raise ValueError(f"Formato desconocido: {fmt}")
return self.exporters[fmt].export(event_id)
# ============================================================
# plugins/ — el rincón raro (visto en la lección 7)
# ============================================================
# base.py → SeatingPlugin: 5 métodos abstractos
# registry.py → SeatingPluginRegistry: register() + get() con importlib
# impls/default_seating.py → DefaultSeating, la única implementación,
# con dos métodos que devuelven True sin hacer nada.
# ============================================================
# data/repository.py — el acceso a datos
# ============================================================
def get_ticket(ticket_id):
row = db.query_one("SELECT * FROM tickets WHERE id = ?", ticket_id)
return Ticket(**row)
def get_order(order_id): ...
def get_customer(customer_id): ...
def get_event(event_id): ...
def save_order(order): ...
def available_seats(event_id): ...
def reserve_seat(seat_id, order_id): ...
# Ningún otro módulo del sistema escribe SQL. Todo pasa por aquí.
# ============================================================
# config.py — la configuración
# ============================================================
class Settings:
_instance = None
def __new__(cls):
# Solo existe una instancia en todo el proceso.
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._load_from_env()
return cls._instance
def _load_from_env(self):
self.STRIPE_KEY = os.environ["STRIPE_KEY"]
self.MP_TOKEN = os.environ["MP_TOKEN"]
self.SEATING_PLUGIN = os.environ.get("SEATING_PLUGIN", "default_seating")
settings = Settings() # importado en once archivos distintos
# ============================================================
# utils/money.py
# ============================================================
SERVICE_FEE_RATE = 0.08
def format_money(amount):
# Todo el sistema guarda montos como float y asume MXN.
return f"${amount:,.2f} MXN"
Ejemplo trabajado: dos entradas del inventario, para que veas el nivel
Antes de que lo hagas tú, voy a escribir dos entradas completas. Una donde el nombre está claro y otra donde no. Fíjate en el formato, en el detalle y sobre todo en el tono: describe, no juzga.
Entrada — payments/ (proveedores de pago)
- Qué hay: una clase
PaymentProvidercon dos métodos que solo lanzan error, y tres clases que la cumplen. Dos de las tres (StripeProvider,MercadoPagoProvider) guardan por dentro el SDK del proveedor y traducen: recibenorder, llaman al SDK con sus nombres y unidades propias, y devuelven unChargeResultque es un tipo nuestro. La tercera (CashProvider) no envuelve nada porque no hay servicio externo: genera una referencia. - Qué problema resuelve: dos, superpuestos. Primero, que el resto del sistema pueda cobrar sin conocer los detalles de cada proveedor —tres formas de decir "cóbrame" detrás de una sola—. Segundo, que el idioma de cada SDK —centavos como entero contra float,
create_chargecontrapay, dos formatos de respuesta distintos— no se filtre fuera de su archivo. - Nombre: dos. La familia de proveedores intercambiables detrás de una interfaz común tiene la forma de una Strategy. Y cada implementación que envuelve un SDK y traduce es un Adapter —la evidencia es que los nombres de afuera y de adentro no coinciden (
chargecontrapay) y hay conversión de argumentos y de respuesta—. - ¿Llegó antes o después del problema? Después. Se ve en el código:
CashProviderno envuelve nada, porque el tercer caso resultó no tener SDK. Una interfaz diseñada de antemano probablemente habría asumido que todos los proveedores cobran contra un servicio externo. Esta aguantó el tercer caso porque se construyó con los casos reales enfrente. - Consecuencias visibles hoy: agregar un cuarto proveedor es crear un archivo. Pero
checkouttodavía elige el proveedor con unif/elif, así que la decisión de cuál construir no está centralizada: cualquier otro lugar que necesite cobrar tendría que repetir ese condicional. - Nivel de confianza: alto. La estructura es clara y la intención se verifica leyendo dos implementaciones.
Entrada — notifications/notifier.py frente a los avisos en checkout
- Qué hay: dos formas distintas de mandar avisos conviviendo. Una es
notify(customer, message), que recorre una listaCHANNELSy manda por todos los canales que apliquen al cliente. La otra son las cinco llamadas directas al final decheckout, que no pasan pornotify(). La primera la usa solo el script de recordatorios; la segunda es la que corre en cada compra. - Qué problema resuelve:
notify()resuelve "manda por todos los medios disponibles sin conocerlos". Las llamadas directas decheckoutno resuelven ningún problema estructural: son la forma original, que quedó cuando alguien escribiónotify()y no migró el checkout. - Nombre: no logro nombrarlo como una sola cosa, y creo que ese es el hallazgo.
notify()tiene forma de Strategy aplicada en conjunto. Las llamadas decheckoutno son ningún patrón. Lo que hay entre las dos —dos caminos para lo mismo, uno nuevo y uno viejo, sin que el viejo se haya retirado— sí tiene un nombre en el vocabulario de problemas, aunque no lo estudiamos todavía: es una migración a medias. - ¿Llegó antes o después del problema? Después, pero se quedó a medio camino. Alguien vio el dolor, escribió la solución y no terminó de aplicarla.
- Consecuencias visibles hoy: una compra y un recordatorio pasan por caminos distintos para hacer lo mismo. Un cambio en la política de canales —por ejemplo, dejar de mandar SMS a quien tiene la app— hay que hacerlo en dos lugares, y es fácil olvidar uno. Además,
checkoutno tiene reintentos:notify()sí los tiene, porque sus canales van envueltos enRetryingChannel, y los del checkout no. - Nivel de confianza: medio en la lectura, alto en el hallazgo. No estoy seguro de si la duplicación fue deliberada; pregunta para el equipo: ¿por qué
checkoutno usanotify()?
Qué esperar de estas dos entradas. Cinco observaciones sobre el formato.
La primera: la sección "qué problema resuelve" viene antes que el nombre, siempre. Es el orden de la lección 5 y es lo que evita etiquetar por la forma.
La segunda: una entrada puede tener dos nombres, porque hay dos patrones superpuestos. Forzar uno solo habría perdido información.
La tercera: la segunda entrada no tiene nombre limpio y es la más valiosa de las dos. Encontró una inconsistencia real que nadie había anotado, y terminó en una pregunta concreta para el equipo. Ese es el mejor resultado posible de un recorrido de dos semanas.
La cuarta: el nivel de confianza está declarado. Distinguir entre lo que verificaste y lo que supones es lo que hace confiable un documento. Un inventario donde todo suena igual de seguro no se puede usar.
Y la quinta: ninguna de las dos entradas propone cambiar nada. La primera nota que la selección del proveedor sigue en un if dentro de checkout; no dice "hay que meter una Factory". Describir el hecho es tu trabajo aquí; proponer es el módulo siguiente.
El proyecto: entrega el inventario de Boletia
Recorre el sistema y entrega un documento con las secciones que siguen. Calcula entre noventa minutos y tres horas; si te toma menos de una hora, probablemente estás etiquetando en vez de leyendo.
Parte 1 — El recorrido (obligatoria)
Una entrada por cada uno de estos rincones. Son nueve:
api/routes.py— cómo entra un pedido y cómo se arma laOrdercheckout/checkout.py— el orquestadorpricing/— el cálculo de precios (y las otras dos funciones que preguntan porkind)payments/— los proveedoresnotifications/— canales, reintentos,notify()y elNotificationManagerreports/— los tres exportadores y elReportManagerplugins/— el registro de asientosdata/repository.py— el acceso a datosconfig.py— la configuración
Cada entrada, con este formato:
### <rincón>
- Qué hay: (la estructura, en dos o tres frases, sin jerga)
- Qué problema resuelve: (en lenguaje de dolor concreto; sin nombrar el patrón)
- Nombre: (si lo tiene; "sin nombrar" es una respuesta válida)
- ¿Antes o después del problema? (y qué evidencia del código lo sugiere)
- Consecuencias visibles hoy: (qué se abarata y qué se encarece, con números si puedes)
- Nivel de confianza: (alto / medio / bajo, y por qué)
Dos reglas duras. La sección "qué problema resuelve" se escribe sin usar el nombre de ningún patrón —si no puedes, todavía no lo entendiste—. Y "sin nombrar" es una respuesta legítima: se espera que al menos dos de las nueve entradas terminen así.
Parte 2 — Lo que sobra y lo que falta (obligatoria)
Dos listas cortas, de dos a cuatro entradas cada una, con una línea de justificación por entrada.
- Estructura que parece sobrar. Lugares donde hay más indirección de la que el problema actual pide. Descríbelo, no propongas todavía cómo quitarlo.
- Estructura que parece faltar. Lugares donde el mismo dolor se repite y no hay nada que lo absorba. Igual: descríbelo, no propongas la solución completa.
Nota la palabra "parece" en los dos títulos. Es deliberada: estás en tu segunda semana y puede haber razones que no conoces. El módulo 8 tiene una lección entera llamada "cuándo dejar el código feo en paz", y viene de aquí.
Parte 3 — Lo que no entiendes (obligatoria)
Entre tres y cinco preguntas concretas para el equipo, del tipo que se pueden contestar. No "¿por qué está tan enredado el checkout?" —eso no es una pregunta, es una queja—, sino "¿por qué checkout no usa notify(), si notify() ya existe?".
Esta parte es obligatoria y no es opcional emocionalmente: entregar un inventario sin huecos, en tu segunda semana, en un sistema de tres años, no es una señal de que entendiste todo. Es una señal de que no miraste con suficiente cuidado.
Parte 4 — El mapa de los ocho (opcional, recomendada)
Toma los ocho patrones de la lección 6 y marca cada uno con: presente (dónde), ausente y no hace falta, o ausente y se nota. Para los del tercer grupo, una línea sobre qué señal te lo indica.
Cómo saber si tu inventario es bueno
No hay una respuesta correcta única, pero sí hay señales de calidad. Revisa tu documento contra estas seis:
1. ¿Las frases del problema se sostienen sin el nombre? Tapa la línea "Nombre" de cada entrada y lee el resto. Si el documento sigue siendo útil, está bien escrito. Si sin los nombres no dice nada, pusiste etiquetas.
2. ¿Hay al menos dos "sin nombrar"? En un sistema real, siempre hay estructuras que no calzan con ningún patrón del catálogo. Si nombraste las nueve, es casi seguro que forzaste alguna.
3. ¿Hay al menos una entrada donde la respuesta correcta es "no hay ningún patrón aquí"? La mayoría del código no es un patrón. Si tu inventario sugiere que todo Boletia está lleno de estructuras nombradas, sobre-etiquetaste.
4. ¿Las consecuencias tienen números o cambios concretos? "Queda más limpio" no es una consecuencia. "Agregar un cuarto proveedor es un archivo nuevo, pero elegirlo sigue siendo un elif en el checkout" sí.
5. ¿Se distingue lo verificado de lo supuesto? El nivel de confianza tiene que variar entre entradas. Si todas dicen "alto", no estás siendo honesto con la lectura.
6. ¿Alguna entrada terminó en una pregunta para el equipo? Las mejores observaciones de una persona nueva terminan así. Es la forma de aportar sin invadir decisiones que no conoces.
Y una prueba final, la más dura: dale tu inventario a alguien que no haya leído el código de Boletia y pregúntale si entiende cómo funciona el sistema. Si puede explicarte de vuelta por dónde pasa una compra, tu documento sirve. Si necesita el código al lado, todavía describiste piezas sueltas en vez de un mapa.
De Boletia a un repositorio de verdad
Boletia es una maqueta —cuidadosamente desprolija, pero maqueta—. Lo que acabas de hacer se traslada casi tal cual a un sistema real, con cuatro diferencias que conviene anticipar.
Es más grande, y por eso se recorre por hilo y no por carpeta. En un sistema de cien mil líneas no puedes inventariar todo. Elige una operación central —el equivalente a "una persona compra un boleto"— y síguela de punta a punta, inventariando solo lo que toca. Un mapa profundo de un hilo vale más que uno superficial de todo.
La historia está en el control de versiones. En un repositorio real puedes preguntar cuándo se introdujo un rincón y qué se dijo en ese momento. Eso contesta directamente la pregunta "¿antes o después del problema?": si la abstracción y su segunda implementación entraron con meses de diferencia, llegó después; si la interfaz entró sola y la segunda implementación nunca llegó, ya sabes qué encontraste. La mecánica de esas consultas es de git-github-guide; aquí solo apunto que la respuesta está ahí.
Hay razones que no están en el código. Un rincón raro puede existir por un cliente que se fue, una auditoría, una limitación de un proveedor que ya no existe. Por eso la Parte 3 son preguntas y no conclusiones.
Y hay una regla social. Entregar en tu segunda semana un documento titulado "todo lo que está mal" no cae bien en ningún equipo, por correcto que sea. El mismo contenido titulado "mi lectura del sistema, con preguntas" abre una conversación. El fondo es idéntico; la puerta por la que entra, no. El oficio de decir estas cosas es de clean-code-and-code-review-guide; el vocabulario para decirlas, de aquí.
Errores comunes
Etiquetar en vez de leer (de método). Qué pasa: alguien recorre los nueve rincones en veinte minutos poniendo nombres de patrón, y entrega un documento donde cada entrada dice "Strategy", "Factory", "Adapter" con una frase de relleno debajo. Se ve completo y no sirve: no distingue lo que está bien de lo que sobra, no tiene consecuencias, y las etiquetas no se pueden verificar. Por qué pasa: nombrar es rápido y satisfactorio, y el mapa de la lección 6 está fresco. Además, un documento con muchos nombres se siente competente. Cómo detectarlo: aplica la señal 1 —tapa los nombres y mira si queda algo—. Cómo corregirlo: escribe siempre la frase del problema primero, y no permitas que ninguna entrada tenga nombre sin tener antes su frase.
Convertir el inventario en una lista de reclamos (de tono). Qué pasa: alguien encuentra el checkout de trescientas líneas, el plugins/ injustificado y la migración a medias, y el documento se convierte en un catálogo de errores ajenos. Aunque cada punto sea cierto, el resultado no se puede usar: quien lo lea se pone a la defensiva, y las observaciones buenas se pierden entre las que suenan a juicio. Por qué pasa: es genuinamente sorprendente encontrar tanto desorden en un sistema en producción, y la sorpresa se filtra al tono. También pasa porque juzgar es más fácil que describir. Cómo detectarlo: cuenta cuántas entradas tuyas contienen la palabra "mal", "feo" o "debería". Si son más de dos, cambiaste de género. Cómo corregirlo: describe el hecho y su consecuencia, sin adjetivo. "Hay una implementación desde hace dos años y entender el mapa de asientos requiere leer tres archivos" es más fuerte que "esto está sobre-diseñado", porque no se puede discutir.
Ocultar lo que no entendiste (de honestidad). Qué pasa: alguien no entiende el registro de plugins o el Settings con __new__, y en vez de anotarlo lo describe vagamente para que no se note, o lo omite. El documento queda con un hueco invisible, que es mucho peor que un hueco declarado. Por qué pasa: la sensación —muy común y muy comprensible— de que admitir una laguna en la segunda semana es admitir que no deberías estar ahí. Cómo detectarlo: si alguna de tus entradas está redactada de forma que suena bien pero no dice nada concreto, ahí hay un hueco tapado. Cómo corregirlo: recuerda que el encargo pidió los huecos. Y recuerda el efecto útil: si tres personas nuevas seguidas no entienden el mismo rincón, eso es un dato sobre el rincón. Tu confusión, escrita con precisión, es evidencia.
Ejercicios
Estos tres ejercicios son parte del proyecto. Puedes hacerlos antes de escribir el inventario completo, como calentamiento.
Ejercicio 1 — Sigue el hilo completo de una compra. Sin escribir todavía ninguna entrada, dibuja o escribe el recorrido de POST /checkout de punta a punta, listando en orden cada módulo que se toca y qué hace ahí. Después responde: (a) ¿cuántos módulos distintos participan? (b) ¿en cuántos puntos del recorrido hay una decisión basada en un texto libre (kind, provider, format)? (c) ¿qué pasa si el envío del correo al organizador falla?
Ver solución
El recorrido:
api/routes.py arma la Order y valida tres reglas sueltas
└─ checkout/checkout.py
├─ pricing/calculator.py precio por boleto según ticket.kind
├─ (cupón) COUPON_RATES, un solo tipo
├─ utils/money.py SERVICE_FEE_RATE y el redondeo
├─ plugins/registry.py si el evento tiene asientos numerados
│ └─ plugins/impls/default_seating.py
├─ payments/… elige proveedor con if/elif y cobra
│ └─ el SDK externo correspondiente
├─ data/repository.py guarda la Order
└─ notifications/… cuatro envíos directos + analytics
└─ api/routes.py serializa y responde
(a) Ocho módulos participan: api, checkout, pricing, utils, plugins, payments, data, notifications, más analytics que es externo. Que una sola operación toque ocho módulos no es malo por sí mismo —es lo esperado en un orquestador—; lo que sí llama la atención es que checkout conozca a los ocho por su nombre.
(b) Tres puntos, y quizás cuatro. ticket.kind en el cálculo de precio; order.provider en la elección del proveedor; settings.SEATING_PLUGIN en el registro de plugins. El cuarto está fuera de este hilo pero es el mismo fenómeno: format en los reportes. Los cuatro son texto libre validado en lugares distintos —o no validado—: routes.py valida el proveedor pero nadie valida kind hasta que calculate_price lanza un error.
(c) Se cae toda la operación después de que el cobro ya salió bien. El cobro ocurrió, la orden se guardó como "paid", y si email_channel.send al organizador lanza una excepción, la petición HTTP falla. El cliente ve un error aunque su compra se procesó; probablemente lo intente de nuevo. Fíjate además en que el aviso al organizador está después del aviso al cliente, así que el cliente sí recibió su correo. El orden importa y nadie lo decidió a propósito: quedó así porque cada aviso se fue agregando al final.
Ese hallazgo —"un aviso que falla tumba un cobro exitoso"— es de los más valiosos que puedes llevar a tu inventario, y no requirió saber ningún nombre de patrón. Salió de seguir el hilo con el dedo.
Ejercicio 2 — Clasifica cinco estructuras sin nombrarlas. Para cada una, escribe solo dos cosas: la frase del problema y si la estructura llegó antes o después del problema, con la evidencia que te lo sugiere.
data/repository.pyconfig.py(la claseSettings)reports/ReportManagerreports/(los tres exportadores)notifications/manager.py(NotificationManager)
Ver solución
1. data/repository.py. Problema: "si el acceso a la base de datos estuviera regado, cambiar una tabla o una consulta obligaría a buscar SQL por todo el sistema, y ninguna otra parte se podría probar sin base de datos". Evidencia de que llegó después: el comentario dice que ningún otro módulo escribe SQL, y las funciones son específicas y con nombres del dominio (available_seats, reserve_seat) en vez de genéricas. Una capa diseñada de antemano suele tener un query(sql) genérico; esta creció por necesidades reales. Nombre, si quisieras darlo: Repository —no es GoF, aparece en la lección 6—.
2. config.py. Problema: "la configuración se lee de variables de entorno y no queremos releerlas ni pasarlas por parámetro a través de todo el sistema". Evidencia sobre el orden: ambigua, y vale la pena decirlo. La instancia única con __new__ es una decisión deliberada de diseño, no algo que emerge. Lo que sí se puede observar es la consecuencia: settings está importado en once archivos, así que once módulos dependen de una variable global. Y _load_from_env corre en el primer uso, lo que significa que las pruebas que quieran otra configuración tienen que pelear con la instancia ya creada. Esta es la estructura que en la lección 6 marcamos como "hay que reconocerla, casi nunca conviene escribirla": el Singleton.
3. ReportManager. Problema: "quien pide un reporte conoce el formato como texto y no debería conocer las clases exportadoras ni cuáles existen". Evidencia de que llegó después: recibe el diccionario EXPORTERS desde fuera en vez de construirlo, lo cual indica que alguien ya tenía los tres exportadores y necesitó un punto de selección. Si hubiera llegado antes, lo más probable es que el diccionario estuviera adentro. Estructuralmente es un punto de selección por tabla —el corazón de un Factory—, con un nombre que no lo dice.
4. Los tres exportadores. Problema, y este es el punto: no resuelven ninguno. Los tres repiten los mismos cuatro pasos y solo cambia el tercero. No hay estructura compartida: hay copia. Evidencia: las dos primeras líneas de los tres métodos son idénticas carácter por carácter. Aquí la respuesta a "¿antes o después?" es "ninguna de las dos: no hay abstracción". Es una de las entradas de "estructura que parece faltar".
5. NotificationManager. Problema: ninguno. Seis métodos sin relación entre sí, sin estado compartido, que no se llaman entre ellos. La prueba: si borras la clase y dejas las seis funciones sueltas en un módulo, no se pierde nada. Evidencia sobre el orden: llegó sin problema, que es la peor variante de "antes". Es una bolsa de funciones con un nombre que suena a orden —emparentada con el God object del módulo 7—.
Por qué funciona: cinco estructuras y cinco respuestas distintas —una que emergió bien, una deliberada y sospechosa, una que emergió con mal nombre, una ausencia, y una que no debería existir—. Ese abanico es lo que hace un inventario útil, y ninguna de las cinco necesitó el nombre del patrón para ser descrita.
Ejercicio 3 — Escribe las preguntas para el equipo. Escribe cinco preguntas concretas y contestables sobre Boletia: del tipo que una persona con contexto puede responder en dos frases. Después marca cuál de las cinco es la que más te ayudaría a entender el sistema, y por qué.
Ver solución
Cinco posibles, con el criterio de que cada una apunta a una decisión que el código no explica:
- "¿Por qué
checkoutno usanotify(), sinotify()ya existe y además tiene reintentos?" — apunta a la migración a medias. - "El registro de plugins de asientos tiene una sola implementación desde hace dos años. ¿Hubo alguna vez una segunda, o algún acuerdo con recintos que no prosperó?" — apunta al rincón sobre-diseñado, y está formulada de manera que no acusa: pregunta por la historia.
- "Si el correo al organizador falla, la petición de checkout responde error aunque el cobro ya se hizo. ¿Es intencional o nadie lo ha visto?" — apunta a un problema real, y ofrece la salida elegante de "nadie lo ha visto".
- "
Order.provideryTicket.kindson texto libre y se validan en lugares distintos. ¿Hubo una razón para no usar un conjunto cerrado de valores?" — apunta a una decisión de modelado que afecta a media docena de condicionales. - "Los tres exportadores repiten las dos primeras líneas idénticas. ¿Se intentó unificarlos alguna vez y no funcionó?" — apunta a una duplicación evidente, y la pregunta "¿se intentó?" es más útil que "¿por qué no lo hicieron?", porque a veces la respuesta es que sí se intentó y hubo una razón.
Cuál ayuda más: probablemente la 2. Las otras cuatro apuntan a cosas que puedes deducir leyendo con más cuidado; la 2 pregunta por información que no está en el código y no puedes obtener de ninguna otra forma —la intención original y qué pasó con ella—. Y es la que más peso tiene hacia adelante: la respuesta decide si plugins/ es candidato a eliminarse en el módulo 2 o si hay un compromiso vivo que lo justifica.
Fíjate en algo que comparten las cinco: ninguna dice "por qué está mal". Todas preguntan por la historia o por la intención. Esa formulación consigue respuestas; la otra consigue defensas.
Por qué funciona: escribir buenas preguntas es una habilidad subestimada y es la forma más rápida de volverte útil en un equipo nuevo. Una pregunta bien hecha le ahorra a la otra persona el trabajo de adivinar qué necesitas, y de paso demuestra que leíste.
Resumen y siguiente paso
En este proyecto recorriste Boletia completa y entregaste un inventario de sus estructuras: nueve rincones con su problema, su nombre cuando lo tiene, si llegó antes o después del problema, sus consecuencias visibles y tu nivel de confianza. Más dos listas —lo que parece sobrar y lo que parece faltar—, tres a cinco preguntas para el equipo, y opcionalmente el mapa de los ocho marcados como presentes, ausentes que no hacen falta, o ausentes que se notan.
Viste el formato de una entrada bien escrita en dos ejemplos: uno donde el nombre estaba claro y había dos patrones superpuestos —Strategy y Adapter en payments/—, y otro donde la respuesta honesta fue "no logro nombrarlo como una sola cosa" y terminó siendo el hallazgo más valioso, con una pregunta concreta para el equipo. Y viste las seis señales de calidad, empezando por la más dura: tapa los nombres y mira si el documento sigue sirviendo.
Con esto cierra el módulo 1. Empezaste sin saber qué es de verdad un patrón y sales con cuatro cosas: la definición de tres casillas —problema, solución, consecuencias— y la conciencia de que la tercera es donde vive el criterio; el vocabulario como el superpoder real, con su fórmula de uso sano y sus tres fallas; el origen de los patrones como descubrimientos y no invenciones, con todo lo que eso implica sobre el orden correcto; la técnica para reconocerlos en código sin etiquetas; el mapa honesto de los ocho que importan; el anti-método y sus cuatro trampas; y un inventario propio de un sistema real.
Lo que sigue es el módulo que hace de contrapeso, y viene a propósito antes de que estudiemos un solo patrón a fondo. El módulo 2 se llama "cuándo NO abstraer" y desarrolla en ocho lecciones lo que la lección 7 enunció en una: por qué cada abstracción tiene un costo continuo, la regla de tres con sus matices y excepciones, por qué la abstracción prematura es el error más caro del oficio, qué significa YAGNI en la práctica, el anti-patrón del plugin para una sola implementación —con plugins/ de Boletia como caso de estudio completo—, y el intercambio entre acoplamiento e indirección, que es el eje real de todas estas decisiones. Su proyecto es el reverso exacto de este: vas a quitar una abstracción que no se gana su lugar, y a defender por qué.
Guarda tu inventario. Lo vas a abrir en la primera lección del módulo 2.
Recursos
- Working Effectively with Legacy Code (Michael Feathers) — el clásico sobre entender y trabajar código heredado. Sus técnicas para trazar dependencias son el complemento natural de este proyecto.
- Your Code as a Crime Scene (Adam Tornhill) — cómo usar la historia del repositorio para descubrir qué partes del sistema duelen de verdad. Contesta empíricamente la pregunta "¿antes o después del problema?".
- Refactoring Guru — catálogo de patrones — para consultar cuando una estructura de tu inventario se parezca a algo y no recuerdes el nombre. Compara siempre por intención, no por diagrama.
- Refactoring Guru — code smells — el otro lado del diccionario, útil para las entradas de "estructura que parece sobrar". Se estudia a fondo en el módulo 7.