Módulo 1: Qué son de verdad los patrones

5. Leer un patrón en código ajeno

Descripción

Al terminar esta lección vas a tener una técnica concreta —cuatro pasos y cuatro pistas— para abrir un archivo que nunca viste y nombrar las estructuras que hay dentro, aunque nadie las haya etiquetado. Vas a practicarla sobre el módulo de notificaciones de Boletia, donde encontrarás dos patrones sin nombre y una cosa que se disfraza de patrón y no lo es. Y vas a salir con algo igual de valioso: saber qué hacer cuando no logras nombrar algo, que es una situación normal y no un fracaso.

Esto importa porque es la habilidad que separa el conocimiento de la lista del conocimiento útil. En el mundo real no hay comentarios que digan "aquí empieza el Decorator". Hay archivos, clases con nombres regulares y decisiones que alguien tomó hace tres años sin explicarlas. Si aprendiste los patrones estudiando diagramas limpios, esa realidad te va a desorientar: las estructuras reales están mezcladas, incompletas, a medio aplicar, y a veces son dos patrones superpuestos en el mismo archivo. Reconocerlas ahí es una habilidad distinta de saber implementarlas, y es la que usas todos los días.

Hay algo más, y es la razón por la que esta lección está aquí y no al final del módulo. La lección 4 estableció que los patrones se descubren, no se inventan. La consecuencia directa para la lectura es que se reconoce un patrón buscando su problema, no su forma. La forma engaña —hay cuatro patrones distintos que se ven como "una clase que envuelve a otra"—, mientras que el problema no: si averiguas para qué está ahí el envoltorio, el nombre sale solo. La técnica de esta lección es, en el fondo, la disciplina de preguntar por el problema antes que por la forma.

Conexión con el módulo: la lección 3 usó el vocabulario en dirección de escritura —transmitir lo que ya reconociste—. Esta lo usa en dirección de lectura, que es la que más se practica. La lección 6 te va a dar el mapa de los pocos patrones que de verdad aparecen, y ese mapa es justamente la lista de cosas que vale la pena aprender a reconocer. La lección 7 advierte contra el efecto secundario de esta habilidad —empezar a ver patrones donde no los hay—. Y el proyecto de la lección 8 es esta técnica aplicada a Boletia completa: recorrerla y entregar un inventario de lo que reconoces y de lo que no.

Las huellas, no el animal

Un buen rastreador casi nunca ve al animal. Ve lo que dejó: una huella en el barro, una rama quebrada a cierta altura, pelo enganchado en una corteza, un montón de tierra removida. Y a partir de esas señales dice, con bastante confianza, qué pasó por ahí, hace cuánto y hacia dónde iba.

Lo interesante es cómo lo hace. No compara la huella contra un catálogo de huellas hasta encontrar la idéntica. Lee la huella preguntándose por el comportamiento: ¿este animal caminaba o corría? ¿tiene garras retráctiles o no? ¿pisó con todo el peso o de paso? Cada respuesta descarta familias enteras. El nombre de la especie es lo último que aparece, y aparece casi solo.

Un rastreador novato hace lo contrario: memoriza fotos de huellas y busca coincidencias. Funciona con la huella perfecta en barro limpio, y falla con lo que hay en el mundo real —huellas a medias, superpuestas, en tierra dura—. Y falla especialmente con las huellas parecidas: hay animales muy distintos que dejan marcas casi iguales, y lo que las separa no es el dibujo sino el contexto —dónde estaba, qué comió, a qué hora—.

Leer patrones en código es exactamente esto. Las huellas son señales estructurales: una forma común repetida, un objeto que contiene a otro, una lista de destinatarios. No ves el patrón; ves lo que dejó. Y como en el rastreo, hay huellas casi idénticas que corresponden a patrones distintos: Adapter, Facade, Decorator y Proxy son los cuatro "una clase que envuelve a otra", y solo se distinguen por para qué está ahí el envoltorio.

De ahí la regla que va a organizar toda la lección: primero la huella, después el problema, y solo al final el nombre. Quien salta directo de la huella al nombre se equivoca la mitad de las veces, y ya vimos en la lección 3 lo que cuesta un nombre equivocado.

Ejemplo trabajado: qué hay dentro de notifications/ de Boletia

Abre el módulo de notificaciones. Nadie te explicó nada. No hay documentación. Los nombres son razonables pero no dicen ningún patrón. Esto es lo que hay:

# ============ notifications/channel.py ============
# Nadie escribió aquí para qué existe esta clase. Está desde el segundo año.

class NotificationChannel:
    def send(self, recipient, message):
        raise NotImplementedError

    def is_available_for(self, customer):
        raise NotImplementedError


# ============ notifications/email_channel.py ============
class EmailChannel(NotificationChannel):
    def send(self, recipient, message):
        smtp.deliver(to=recipient, subject=message.subject, body=message.html)

    def is_available_for(self, customer):
        # Todo cliente tiene correo: es obligatorio al registrarse.
        return True


# ============ notifications/sms_channel.py ============
class SmsChannel(NotificationChannel):
    def send(self, recipient, message):
        # El proveedor de SMS corta a 160 caracteres, así que mandamos la versión corta.
        sms_api.post(number=recipient, text=message.short_text[:160])

    def is_available_for(self, customer):
        return customer.phone is not None


# ============ notifications/push_channel.py ============
class PushChannel(NotificationChannel):
    def send(self, recipient, message):
        push_api.notify(token=recipient, title=message.subject, body=message.short_text)

    def is_available_for(self, customer):
        return customer.push_token is not None


# ============ notifications/retrying.py ============
# Agregado el año pasado, después de que el proveedor de SMS se cayó un viernes.

class RetryingChannel(NotificationChannel):
    def __init__(self, inner, attempts=3, wait_seconds=2):
        self.inner = inner          # otro NotificationChannel
        self.attempts = attempts
        self.wait_seconds = wait_seconds

    def send(self, recipient, message):
        last_error = None
        for _ in range(self.attempts):
            try:
                return self.inner.send(recipient, message)
            except TransientError as e:
                # Solo reintentamos errores transitorios. Un número inválido no mejora
                # por reintentarlo, así que ese error se propaga tal cual.
                last_error = e
                sleep(self.wait_seconds)
        raise last_error

    def is_available_for(self, customer):
        # Delega: si el canal envuelto no aplica, este tampoco.
        return self.inner.is_available_for(customer)


# ============ notifications/notifier.py ============
CHANNELS = [
    RetryingChannel(EmailChannel()),
    RetryingChannel(SmsChannel()),
    PushChannel(),                    # push no se reintenta: si falla, no importa mucho
]

def notify(customer, message):
    # Manda el mensaje por todos los canales que apliquen a este cliente.
    for channel in CHANNELS:
        if channel.is_available_for(customer):
            channel.send(recipient_for(channel, customer), message)


# ============ notifications/manager.py ============
# Agregado por alguien que quería "ordenar" el módulo. Nadie lo usa mucho.

class NotificationManager:
    def format_currency(self, amount): ...
    def build_confirmation(self, order): ...
    def build_organizer_alert(self, order, event): ...
    def log_delivery(self, customer_id, channel_name): ...
    def get_template(self, name): ...
    def validate_phone(self, phone): ...

Vamos a leerlo como un rastreador. Sin buscar nombres todavía.

Primera huella: hay una forma común y varias cosas que la cumplen. NotificationChannel no hace nada: sus dos métodos lanzan un error. Es puro contrato —"todo canal sabe enviar y sabe decir si aplica a un cliente"—. Y hay tres clases que lo cumplen con implementaciones distintas.

¿Qué problema resuelve eso? Míralo desde notify(). Esa función recorre canales y les habla igual a todos, sin saber cuál es cuál. No hay ningún if channel == "sms" ahí. El problema resuelto es: "quiero mandar un aviso por todos los medios que apliquen, sin que el código que decide eso tenga que conocer los detalles de cada medio, y sin tener que tocarlo cuando entre un medio nuevo".

Ahora sí, el nombre. Una familia de comportamientos intercambiables detrás de una forma común es el terreno de Strategy. Con un matiz honesto: en la Strategy de manual se elige una estrategia y se usa; aquí se usan todas las que apliquen. La estructura es la de una Strategy y el uso se parece más a una lista de manejadores. En un inventario yo lo anotaría así: "familia de canales intercambiables con interfaz común —forma de Strategy—, aplicados en conjunto en vez de elegir uno". Esa frase es más útil que forzar una etiqueta limpia, y es exactamente el tipo de honestidad que el proyecto del módulo te va a pedir.

Segunda huella: hay un objeto que contiene a otro del mismo tipo. RetryingChannel recibe un inner que es otro NotificationChannel, y él mismo es un NotificationChannel. Esa combinación —envuelve a uno de su especie y se hace pasar por uno de su especie— es una huella muy distintiva. Fíjate además en is_available_for, que simplemente delega al de adentro.

¿Qué problema resuelve? Agregar reintentos a un canal sin tocar el código del canal. EmailChannel no sabe que lo están reintentando; su código no cambió ni una línea. Y el reintento es opcional por canal: en CHANNELS se ve que correo y SMS van envueltos y push no.

El nombre: Decorator. Un objeto que envuelve a otro de la misma forma para agregarle comportamiento, de modo que el envuelto no se entera y quien lo usa tampoco. Y aquí es donde la disciplina de preguntar por el problema paga: si te hubieras quedado en la forma —"una clase que envuelve a otra"— habrías dudado entre cuatro patrones. Al preguntar para qué envuelve —"para agregar comportamiento manteniendo la misma interfaz"— el nombre queda determinado.

Tercera huella: un lugar donde se arma la configuración. La lista CHANNELS en notifier.py es donde se decide qué canales existen y cuáles llevan reintento. Es pequeña, pero es un punto de creación y merece anotarse: es el lugar donde alguien tendrá que tocar para agregar un canal, y el lugar donde se cambia la política de reintentos.

¿Tiene nombre? No exactamente. Es un punto de composición hecho a mano, y está bien que lo sea: tres canales no ameritan una fábrica. En el inventario yo escribiría "punto de composición explícito, sin ceremonia. Adecuado al tamaño". Reconocer que algo no necesita un patrón también es leer bien el código.

Cuarta observación: algo que parece patrón y no lo es. NotificationManager tiene seis métodos que no comparten nada: formatear moneda, armar dos mensajes distintos, registrar entregas, leer plantillas y validar teléfonos. No hay estado compartido, no hay una responsabilidad única, no hay una forma común con implementaciones. Es una bolsa de funciones sueltas metidas en una clase porque la palabra "Manager" sonaba ordenada.

Esto no es un patrón: es una señal de olor, y tiene su propio nombre en el vocabulario —está emparentado con el God object, que veremos en el módulo 7—. La pista que lo delata es doble: un nombre genérico terminado en Manager, Helper, Utils o Handler, y métodos que no se llaman entre sí ni comparten estado. Si borraras la clase y dejaras las seis funciones sueltas en un módulo, no se perdería nada. Esa prueba mental —"¿la clase aporta algo o solo agrupa?"— es rápida y sirve siempre.

Qué esperar de este recorrido. Cinco observaciones que valen más que el resultado.

La primera: nunca busqué en un catálogo. Miré la estructura, formulé el problema, y el nombre apareció al final. Ese orden es la técnica.

La segunda: la palabra "patrón" no aparece en ningún lado del código, y eso es completamente normal. Un módulo de notificaciones que se llamara strategy_channels.py sería raro, no bueno. La estructura se reconoce por su forma y su intención, no por su nombre.

La tercera: uno de los hallazgos no tuvo etiqueta limpia. La familia de canales tiene la forma de una Strategy pero se usa como una cadena de manejadores. Escribir eso con precisión es mejor que forzar un nombre. En un equipo, decir "esto tiene forma de Strategy pero se aplican todos, no se elige uno" es un comentario más útil que "esto es una Strategy" a secas.

La cuarta: uno de los hallazgos fue un problema, no un patrón. Parte de leer estructura es reconocer las estructuras malas, y esas también tienen vocabulario. El módulo 7 entero se ocupa de ese lado del diccionario.

Y la quinta, que quiero dejarte grabada: el mismo código puede tener dos patrones superpuestos. RetryingChannel es un Decorator que decora miembros de una familia estilo Strategy. Eso es normal en código real y confunde a quien estudió los patrones aislados, uno por diagrama. Los patrones se combinan; no vienen en compartimentos.

Las cuatro pistas que se buscan

De ese recorrido salen cuatro señales que cubren la gran mayoría de lo que vas a encontrar. Apréndelas como formas, no como nombres.

Pista 1 — Una forma común con varias implementaciones. Buscas: una clase base, una interfaz, un protocolo o simplemente un conjunto de clases con los mismos métodos; y dos o más cosas que la cumplen. En Python muchas veces no hay clase base explícita: hay tres clases con el mismo método y ya. La pregunta que sigue: "¿quién las usa, y sabe cuál le tocó?". Si no lo sabe, hay desacoplamiento intencional. Familia de patrones probable: Strategy, State, Template Method si además comparten un esqueleto, y a veces el lado consumidor de un Factory.

Pista 2 — Un objeto que guarda a otro y se le parece. Buscas: un constructor que recibe un objeto y lo guarda en un atributo (self.inner, self.wrapped, self.target, self.client), y métodos que delegan en él. La pregunta que sigue es la que decide todo: "¿por qué está en medio?". Cuatro respuestas posibles, cuatro patrones distintos, y los veremos en tabla más abajo.

Pista 3 — Una lista de cosas a las que se avisa. Buscas: una colección de destinatarios (subscribers, listeners, handlers, observers, callbacks), métodos para agregarse y quitarse de ella, y un bucle que la recorre disparando algo. La pregunta que sigue: "¿quién publica sabe quién escucha?". Si no lo sabe, es desacoplamiento por evento. Familia probable: Observer, y su primo el sistema de eventos.

Pista 4 — Un lugar donde se decide qué construir. Buscas: una función o método cuyo único trabajo es devolver un objeto ya armado, normalmente con un if, un diccionario de tipos o un registro; nombres como create_, make_, build_, get_, for_. La pregunta que sigue: "¿quién llama a esto sabe qué tipo recibe?". Familia probable: Factory en sus variantes, y Builder si la construcción se hace por pasos.

Hay una quinta pista, más débil pero útil: los nombres. Sufijos como -Factory, -Adapter, -Strategy, -Listener, -Observer, -Builder, -Proxy a veces anuncian la intención. Con dos advertencias serias. La primera: en código real casi nunca están, porque la gente nombra por dominio (EmailChannel) y no por patrón (EmailNotificationStrategy) —y hace bien—. La segunda, más importante: cuando están, mienten con frecuencia. Vas a encontrar clases llamadas SomethingFactory que no construyen nada y SomethingAdapter que no adaptan nada, porque alguien puso el sufijo por costumbre. Trata el nombre como una hipótesis, nunca como una confirmación: verifica siempre contra la estructura.

La técnica, en cuatro pasos

Este es el orden en que conviene mirar un módulo desconocido. Sirve tal cual para el proyecto de la lección 8.

Paso 1 — Lee la forma antes que el contenido. Antes de abrir un archivo, mira la lista de archivos. Una carpeta con una clase base y tres implementaciones se ve así desde afuera: channel.py, email_channel.py, sms_channel.py, push_channel.py. Ese solo listado ya sugiere la pista 1. Los nombres de archivo y su agrupación te dan la primera hipótesis gratis, antes de leer una línea.

Paso 2 — Busca las cuatro pistas, sin nombrarlas. Recorre el módulo anotando estructuras, no patrones. "Hay una clase base con tres implementaciones." "Hay una clase que guarda otra del mismo tipo." "Hay una lista que se recorre avisando." Resiste la tentación de nombrar todavía. Es una tentación fuerte y ceder a ella es la fuente principal de nombres equivocados.

Paso 3 — Sigue una llamada de punta a punta. Elige una operación real —"un cliente compra y recibe su confirmación"— y síguela con el dedo desde donde entra hasta donde termina. Este paso es el que te da la intención, que es lo que las pistas por sí solas no dan. Al seguir el hilo descubres que RetryingChannel está en medio pero no cambia el mensaje ni la interfaz: solo insiste. Esa observación es la que decide entre Decorator y Proxy, y solo se obtiene mirando el flujo, no la declaración de la clase.

Un truco práctico: si el lenguaje o el editor te lo permite, haz la pregunta al revés. En lugar de "¿qué llama esta función?", pregunta "¿quién llama a esta función?". Para entender la intención de una pieza, saber quién la usa dice más que saber qué hace.

Paso 4 — Formula el problema, y solo entonces busca el nombre. Escribe en una frase, sin usar jerga, qué incomodidad resuelve la estructura que encontraste: "permite mandar por todos los medios que apliquen sin que el que manda conozca los medios". Con esa frase en la mano, el nombre es casi automático —y si no lo es, tu frase sigue siendo un hallazgo perfectamente válido para el inventario—.

Ese último punto merece énfasis. La frase del problema es el entregable; el nombre es el bono. Si terminas un recorrido con diez frases de problema y solo seis nombres, hiciste un buen trabajo. Si terminas con diez nombres y ninguna frase, no entendiste el módulo: le pusiste etiquetas.

Cuando no logras nombrarlo

Te va a pasar seguido, y quiero quitarle el drama ahora, porque el proyecto del módulo lo pide explícitamente.

Hay cuatro razones por las que una estructura no se deja nombrar, y ninguna es que tú no sepas suficiente.

Porque no es un patrón. La mayoría del código no lo es. Una función que valida un teléfono no es nada: es una función que valida un teléfono. Buscar un patrón en cada rincón es el error que la lección 7 llama, con razón, síndrome del martillo nuevo.

Porque es un patrón a medias. Alguien empezó una estructura y la dejó incompleta —una interfaz con una sola implementación, un sistema de eventos con un solo suscriptor, una fábrica que solo tiene un caso—. En código real esto es frecuentísimo, y anotarlo así es más valioso que nombrarlo: "tiene forma de X pero le falta la variedad que lo justificaría" es exactamente la observación que el módulo 2 va a convertir en decisión.

Porque son dos patrones superpuestos. Ya lo viste: un Decorator envolviendo miembros de una familia estilo Strategy. Cuando dos estructuras conviven, ningún nombre solo describe lo que hay.

Porque es una estructura propia del dominio. Muchos equipos desarrollan formas recurrentes que solo tienen sentido en su negocio y que no están en ningún catálogo. Son patrones locales legítimos, y descubrirlos —"aquí siempre se hace así"— es entender el sistema.

En los cuatro casos, la acción correcta es la misma: describe la estructura y el problema en prosa, y márcalo como sin nombrar. Un inventario que dice honestamente "no logro nombrar esto, pero hace tal cosa" es infinitamente más útil que uno que pone una etiqueta aproximada. Y en un equipo, esa honestidad tiene un efecto secundario bueno: alguien con más contexto lee tu descripción y dice "ah, eso es X porque…", y ahí aprendiste algo real.

Huellas parecidas: las confusiones frecuentes

Estas son las parejas que más se confunden, y cómo se distinguen. Nota que la columna que decide siempre es la de la intención, nunca la de la forma.

Los cuatro que se ven como "una clase que envuelve a otra":

PatrónMisma formaQué lo distingue (la intención)Pista al leer
AdapterTraduce una interfaz a otra que ya existía en tu códigoLos nombres de los métodos de afuera y de adentro son distintos; hay conversión de argumentos
DecoratorAgrega comportamiento manteniendo la misma interfazLos métodos de afuera y de adentro se llaman igual; hay algo antes o después de delegar
FacadeOfrece una interfaz nueva y simple sobre un conjunto complicadoEnvuelve varias cosas, no una; sus métodos hacen varias llamadas internas
ProxyControla el acceso al de adentro, sin agregar funcionalidad de negocioMisma interfaz, y lo que hace de más es cachear, autorizar, medir o retrasar la creación

Un caso concreto para fijar la diferencia entre los dos que más se confunden. Si RetryingChannel tuviera un método deliver() que por dentro llama a inner.send(), sería un Adapter —cambió el nombre de la operación—. Como tiene send() que llama a inner.send() y agrega reintentos, es un Decorator. Un método distinto y cambia el nombre del patrón; por eso la lectura tiene que ser detallada.

Y los tres que se ven como "una familia de comportamientos":

PatrónMisma formaQué lo distinguePista al leer
StrategySe elige desde fuera cuál usar, y se puede cambiarQuien la usa la recibe como parámetro o atributo
StateEl objeto cambia solo de comportamiento según su estado internoLas variantes se reemplazan a sí mismas: una transición asigna la siguiente
Template MethodParecidaEl esqueleto es fijo y solo varían algunos pasosHay un método largo en la clase base que llama a métodos vacíos que las hijas rellenan

La prueba rápida entre Strategy y State: pregúntate quién decide el cambio. Si lo decide alguien de afuera, es Strategy. Si el propio objeto se cambia a sí mismo al ocurrir algo, es State. En Boletia, las reglas de precio las decide el tipo de boleto desde fuera —Strategy—; el ciclo de una Order que pasa de pending a paid a cancelled, si estuviera modelado con objetos, sería State.

Errores comunes

Saltar de la forma al nombre (de técnica). Qué pasa: alguien ve una clase que guarda otra y dice "Decorator" sin verificar. La mitad de las veces era un Adapter, un Proxy o simplemente composición normal —que no es ningún patrón—. Después transmite ese nombre a su equipo con la confianza del que leyó el código, y ya vimos en la lección 3 lo que cuesta eso. Por qué pasa: la forma se ve en dos segundos y la intención exige seguir el flujo, que toma diez minutos. Y una vez que un nombre aparece en tu cabeza, sesga todo lo que lees después. Cómo detectarlo: si nombraste sin haber podido decir para qué está ahí la estructura, saltaste el paso 3. Cómo corregirlo: obligarte a escribir la frase del problema antes del nombre. Si la frase no sale, el nombre tampoco debería salir.

Buscar patrones en todas partes (de criterio). Qué pasa: después de aprender la técnica, alguien abre un archivo y trata de nombrar cada clase. Termina llamando "Facade" a una función que agrupa dos llamadas y "Factory" a un constructor normal. El inventario resultante es ruido: no distingue lo importante de lo trivial. Por qué pasa: es el efecto del vocabulario nuevo —empiezas a ver la palabra en todas partes— y también una idea equivocada sobre qué hace bueno a un inventario, como si más entradas fuera mejor. Cómo detectarlo: si más de la mitad de las clases de un módulo terminaron con nombre de patrón, estás sobre-etiquetando. En código sano, la mayoría de las clases no son ningún patrón: son clases. Cómo corregirlo: aplica el filtro del problema. Si no puedes decir qué incomodidad concreta resuelve esa estructura, no la etiquetes. La lección 7 desarrolla esto.

Confiar en el nombre de la clase (de técnica). Qué pasa: alguien encuentra PaymentAdapter y anota "hay un Adapter" sin leerla. Resulta que no adapta nada —las dos interfaces ya eran iguales— y que en realidad es un Proxy que agrega un registro de auditoría. El inventario queda mal y, peor, nadie lo va a revisar porque el nombre "confirma" la etiqueta. Por qué pasa: el sufijo parece una declaración de intención del autor original, y a veces lo es. Pero también hay mucho sufijo puesto por costumbre, copiado de otro proyecto, o correcto en su momento y desactualizado después de tres refactorizaciones. Cómo detectarlo: si tu evidencia para un nombre es el nombre, no tienes evidencia. Cómo corregirlo: trata los sufijos como hipótesis y verifica contra la estructura. Y cuando encuentres una discrepancia, anótala: "se llama PaymentAdapter pero no adapta; hoy es un Proxy de auditoría". Ese tipo de hallazgo es oro en una revisión, porque los nombres mentirosos son de las cosas que más confunden a quien llega nuevo.

Ejercicios

Ejercicio 1 — Identifica la huella y la intención. Para cada fragmento, escribe: (a) qué pista estructural ves, (b) la frase del problema que resuelve —sin jerga—, (c) el nombre, si te atreves, o "sin nombrar" con tu justificación.

# --- Fragmento A: payments/mercadopago_provider.py ---
class MercadoPagoProvider:
    def __init__(self, client):
        self.client = client            # el SDK oficial del proveedor

    def charge(self, order):
        # Nuestro código habla de charge(order). El SDK habla de pay(amount, description).
        result = self.client.pay(order.total, description=f"Boletia #{order.id}")
        # Y devuelve su propio formato; lo traducimos al nuestro.
        return ChargeResult(ok=result["approved"], reference=result["payment_id"])


# --- Fragmento B: reports/cached_exporter.py ---
class CachedExporter:
    def __init__(self, inner):
        self.inner = inner

    def export(self, event_id):
        key = f"report:{event_id}"
        hit = cache.get(key)
        if hit:
            return hit                  # ya lo generamos hace poco
        path = self.inner.export(event_id)
        cache.set(key, path, ttl=300)
        return path


# --- Fragmento C: pricing/calculator.py ---
def total_for(order):
    subtotal = sum(calculate_price(t, order.created_at) for t in order.tickets)
    fee = subtotal * SERVICE_FEE_RATE
    return round(subtotal + fee, 2)
Ver solución

Fragmento A. (a) Pista 2: un objeto guarda a otro (self.client) y delega en él. (b) El problema: "el SDK del proveedor habla un idioma distinto al de nuestro código —otros nombres de método, otros argumentos, otro formato de respuesta— y no queremos que ese idioma se filtre al resto del sistema". (c) Adapter. La evidencia es doble y decisiva: el método de afuera se llama charge y el de adentro pay —cambió el nombre—, y además hay traducción del resultado a un tipo propio. Eso es traducir una interfaz a otra que ya existía, que es exactamente Adapter y no Decorator.

Fragmento B. (a) Pista 2 otra vez: envuelve a otro y tiene el mismo método export. (b) El problema: "generar el reporte es caro y varias personas lo piden seguido; queremos evitar regenerarlo sin que el exportador ni quien lo llama se enteren". (c) Proxy, específicamente un proxy de caché. La distinción con Decorator es fina y vale la pena: el Decorator agrega comportamiento al resultado o al proceso; el Proxy controla el acceso al objeto de adentro —aquí, a veces ni siquiera lo llama—. La pista más fuerte es esa: hay un camino en el que self.inner nunca se ejecuta. Un Decorator siempre delega; un Proxy puede decidir no hacerlo. Si respondiste Decorator, no está mal como primera lectura: es de las confusiones más razonables que existen, y el hallazgo importante —"hay un envoltorio que evita trabajo repetido"— lo tienes igual.

Fragmento C. (a) Ninguna pista. No hay forma común, ni envoltorio, ni lista de suscriptores, ni punto de creación. (b) El problema: sumar los precios y agregar la comisión de servicio. (c) No es ningún patrón, y está bien. Es una función que hace una cuenta. Este fragmento está aquí a propósito: la respuesta correcta más frecuente al leer código es "esto no es nada". Si te dieron ganas de llamarlo Facade porque "agrupa dos operaciones", ese impulso es exactamente el que la próxima lección te va a pedir domesticar.

Ejercicio 2 — Sigue el hilo y decide. Vuelve al módulo notifications/ completo de esta lección. Sin volver a leer mi análisis, responde: (a) si PushChannel empieza a fallar seguido y quieres darle reintentos, ¿qué archivos tocas y cuántas líneas? (b) si entra un canal nuevo —WhatsApp—, ¿qué archivos tocas? (c) ¿qué te dice esa respuesta sobre si la estructura se gana su lugar?

Ver solución

(a) Un archivo, una línea. En notifications/notifier.py, la lista CHANNELS pasa de PushChannel() a RetryingChannel(PushChannel()). No se toca push_channel.py ni retrying.py. Eso es exactamente lo que compra el Decorator: comportamiento nuevo sobre algo existente sin modificar ni el existente ni a quien lo usa.

(b) Dos archivos. Uno nuevo, whatsapp_channel.py, con las dos operaciones (send y is_available_for); y una línea agregada a CHANNELS. notify() no cambia. Ninguno de los otros canales cambia. checkout no cambia.

(c) Que sí se la gana, claramente. La prueba no es que la estructura sea elegante: es que los dos cambios más probables cuestan poco y están localizados. Compara con la alternativa sin estructura —un notify() con un if por canal y el reintento copiado dentro de dos ramas—: agregar WhatsApp tocaría la función central, y agregar reintento a push implicaría copiar el bloque de reintentos una tercera vez.

Y hay un detalle que conviene notar, porque es el que hace honesto el análisis: hay tres canales, no uno. La misma estructura con un solo canal sería el rincón plugins/. No es la forma lo que la justifica; es el número de variantes reales y la frecuencia con que entran nuevas.

Por qué funciona: "¿qué archivos toco para el cambio más probable?" es la mejor pregunta que existe para evaluar una estructura, y no requiere saber ningún nombre. Es la que vas a usar en el módulo 2 para decidir qué se queda y qué se va.

Ejercicio 3 — Encuentra el nombre mentiroso. Aquí hay tres clases de Boletia con sus nombres reales. Para cada una, decide si el nombre corresponde a lo que hace, y escribe la línea que pondrías en tu inventario.

# --- 1 ---
class TicketFactory:
    # Único método de la clase.
    def create(self, event_id, kind, price):
        return Ticket(event_id=event_id, kind=kind, base_price=price, status="available")

# --- 2 ---
class ReportManager:
    def __init__(self, exporters):
        self.exporters = exporters      # dict: "csv" -> CsvExporter(), etc.

    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)

# --- 3 ---
class SeatingPluginRegistry:
    _plugins = {}

    @classmethod
    def register(cls, name, plugin_cls):
        cls._plugins[name] = plugin_cls

    @classmethod
    def get(cls, name):
        # Importa el módulo por nombre y espera que se auto-registre al importarse.
        if name not in cls._plugins:
            importlib.import_module(f"plugins.impls.{name}")
        return cls._plugins[name]()
Ver solución

1. El nombre miente por exceso. TicketFactory no decide nada: no hay tipos que elegir, no hay lógica, no hay variantes. Es un constructor con pasos extra que igual podría ser el __init__ de Ticket o una función suelta. Una fábrica existe para decidir qué construir; esto solo construye. Línea de inventario: "TicketFactory — no es una Factory: no hay decisión, es un constructor con un valor por defecto. El sufijo confunde. Candidata a ser una función o el propio constructor."

2. El nombre miente por defecto. Se llama ReportManager —un nombre vacío, del mismo tipo que NotificationManager—, pero lo que hace sí tiene nombre: recibe un mapa de formato a exportador y devuelve el que corresponde. Eso es un punto de selección, o sea el corazón de un Factory en su versión de tabla, y además delega. La estructura está bien; el nombre no dice nada. Línea de inventario: "ReportManager — funciona como Factory por tabla: mapea formato a exportador y delega. Estructura sana, nombre poco informativo; ExporterRegistry o ExporterSelector describirían mejor."

3. El nombre es honesto y ese es justamente el problema. Sí es un registro de plugins, con descubrimiento dinámico por nombre de módulo y auto-registro al importar. La estructura hace lo que dice. Lo que hay que anotar no es un error de nombre sino un error de escala: es el rincón plugins/ de Boletia, con una sola implementación desde hace dos años. Y tiene un costo extra que se ve en el código: como el módulo se importa por nombre construido en tiempo de ejecución, ninguna herramienta —ni el editor, ni el buscador, ni un analizador estático— puede decirte quién implementa esto. Línea de inventario: "SeatingPluginRegistry — registro de plugins con carga dinámica. Correcto en su forma, injustificado en su escala: una implementación desde hace dos años. Además, la carga por nombre rompe la navegación del editor. Candidato número uno a eliminar."

Por qué funciona: los tres casos son los tres tipos de discrepancia que vas a encontrar —nombre que promete de más, nombre que no promete nada, y nombre correcto sobre una estructura injustificada—. Y fíjate que en los tres, la línea de inventario dice qué es, qué evidencia hay y qué harías. Ese formato es el que el proyecto de la lección 8 va a pedirte.

Resumen y siguiente paso

En esta lección aprendiste a leer estructura en código ajeno como un rastreador lee huellas: primero la huella, después el problema, y solo al final el nombre. Viste que ese orden importa porque la forma engaña —hay cuatro patrones distintos que se ven como "una clase que envuelve a otra"— y lo único que los distingue es para qué está ahí el envoltorio.

Recorriste el módulo notifications/ de Boletia sin ninguna etiqueta a la vista y encontraste cuatro cosas: una familia de canales intercambiables con forma de Strategy —aunque se aplican todos en vez de elegir uno, y decirlo así es más honesto que forzar la etiqueta—; un RetryingChannel que envuelve a otro canal manteniendo su interfaz, o sea un Decorator; un punto de composición explícito que no necesita ninguna ceremonia; y un NotificationManager que no es un patrón sino una bolsa de funciones con nombre ordenado.

Te llevas las cuatro pistas —forma común con varias implementaciones, objeto que envuelve a otro, lista de destinatarios, lugar donde se decide qué construir—, la advertencia de que los sufijos en los nombres son hipótesis y no confirmaciones, y la técnica de cuatro pasos: lee la forma de los archivos, anota pistas sin nombrarlas, sigue una llamada de punta a punta para obtener la intención, y formula el problema antes que el nombre. Más las tablas de huellas parecidas, donde la columna que decide siempre es la intención. Y sobre todo: no lograr nombrar algo es un resultado válido, y describirlo en prosa vale más que ponerle una etiqueta aproximada.

Antes de avanzar deberías poder: recorrer un módulo desconocido y anotar sus estructuras sin nombrarlas; distinguir Adapter de Decorator por el nombre de los métodos y la traducción de argumentos; y explicar por qué la frase del problema es el entregable y el nombre es el bono.

Ya sabes reconocer. Falta saber qué vale la pena reconocer. El catálogo tiene docenas de patrones y no todos aparecen con la misma frecuencia —ni de lejos—. La lección 6 dibuja el mapa honesto: los ocho que se encuentran constantemente en código real, las tres familias que sirven para ordenarlos mentalmente, y cuáles son cultura general que puedes dejar para después sin ninguna culpa.

Recursos