Módulo 4: Patrones para crear objetos
1. Presentación del módulo: el problema de "cómo se construye esto"
Descripción
Al terminar esta lección vas a poder nombrar un problema que probablemente ya sufriste sin tener palabras para él: la decisión de qué objeto construir se reparte por el sistema y se duplica. Vas a ver ese problema en vivo en Boletia —el mismo if que elige el proveedor de pago, copiado en cuatro archivos distintos— y vas a entender por qué esa duplicación es de un tipo especialmente traicionero: no es código repetido cualquiera, es conocimiento repetido. Y vas a salir con el mapa del módulo: qué patrón de esta familia resuelve qué parte del problema, y cuál de ellos está aquí solo para que aprendas a reconocerlo y evitarlo.
Esto importa por una razón práctica y aburrida, que es justo la razón por la que importa. En el módulo 3 aprendiste a separar el comportamiento que varía: las reglas de precio de Boletia dejaron de vivir en un condicional de ochenta líneas y pasaron a ser piezas intercambiables, cada una en su archivo, cada una probable por separado. Excelente. Pero al hacer eso apareció una pregunta nueva que antes no existía: si ahora hay cuatro piezas intercambiables, ¿quién decide cuál se usa? Esa pregunta no la resuelve la Strategy. La Strategy define el contrato y las implementaciones; deja abierto quién elige. Y si nadie se hace cargo de esa elección de forma deliberada, la elección se reparte —un if aquí, otro allá— y terminas con el problema que veníamos huyendo, mudado de lugar.
Los patrones de esta familia se llaman "creacionales" en el catálogo clásico, y ese nombre es exacto pero suena a manual. Prefiero decirlo así: son los patrones que responden "¿quién decide qué se construye, y con qué?". Factory concentra la decisión. Builder ordena la construcción cuando construir es complicado. La inyección de dependencias saca la construcción de adentro del objeto que la necesita. Y Singleton —el cuarto— es el que está aquí para que lo reconozcas cuando lo veas y sepas por qué no vas a escribirlo.
Conexión con el módulo: esta lección instala el problema; las seis siguientes lo resuelven por partes. La lección 2 define Factory en su forma útil —una función que, dado un dato, devuelve el objeto correcto— y separa esa forma simple de las variantes ceremoniales del catálogo. La lección 3 es el caso trabajado completo sobre los proveedores de pago de Boletia, y muestra cómo Factory y la Strategy del módulo 3 se usan juntas, que es como aparecen en código real. La lección 4 pasa al segundo problema: cuando construir un objeto requiere muchos datos y algunos opcionales, y ahí aparece Builder —con la advertencia de que en Python un dataclass con argumentos por nombre ya resuelve la mayoría de esos casos. La lección 5 es la del sospechoso: Singleton, tratado con la honestidad que casi nunca recibe. La lección 6 explica la inyección de dependencias sin jerga y sin framework, porque en Python es literalmente pasar un parámetro. La lección 7 es la vacuna del módulo, en la línea del módulo 2: la mayoría de los objetos no necesitan nada de esto. Y la lección 8 —el proyecto— te pide ordenar la creación dispersa de PaymentProvider y NotificationChannel con el patrón mínimo que resuelva el problema, y dejar por escrito qué decidiste no abstraer.
Cuatro personas comprando el mismo papel
Imagina una oficina de treinta personas. Nadie definió nunca quién compra los materiales, así que se resolvió como se resuelven estas cosas: cada área compra lo suyo. Contabilidad le compra papel a la papelería de la esquina. Diseño le compra a una tienda en línea porque necesita gramaje especial. Recursos humanos le compra al mismo proveedor de siempre porque tienen crédito. Y la recepción compra en el súper cuando se acaba.
Durante un año esto funciona sin que nadie lo note. Y un día pasa lo inevitable: el proveedor de la esquina cierra. Ahora alguien tiene que avisarle a las cuatro áreas, y las cuatro tienen que decidir a dónde se cambian. Se le avisa a tres. La cuarta sigue mandando órdenes de compra a un local vacío durante dos meses.
Fíjate en dónde estuvo el problema de verdad. No estuvo en que compraran papel —todas necesitaban papel, con razón—. El problema fue que la decisión de a quién comprarle estaba en cuatro cabezas distintas. Y una decisión repartida tiene una propiedad matemática desagradable: para cambiarla hay que cambiarla en todos lados, y la probabilidad de olvidar uno crece con el número de lugares. Con dos lugares casi nunca falla. Con cuatro, falla a veces. Con diez, falla siempre.
La solución que adopta esa oficina no es glamorosa y por eso es buena: se nombra a una persona de compras. Las áreas dejan de saber a quién se le compra. Dicen "necesito papel bond tamaño carta" y les llega. Cuando el proveedor cambia, cambia en un solo lugar, y ninguna área se entera —que es exactamente el punto—.
Eso es, sin adornos, lo que hace una Factory. Y nota lo que la persona de compras no hace: no fabrica el papel, no decide cuánto papel necesita contabilidad, no usa el papel. Solo concentra una decisión que estaba repartida. Es un cambio pequeño y aburrido, y esas suelen ser las mejores decisiones de diseño.
Guarda también la otra mitad de la historia, porque la vamos a necesitar en la lección 7. Si la oficina tuviera una sola área y comprara papel a un solo proveedor, nombrar a una persona de compras sería agregar burocracia sin ganar nada. La misma solución, en el contexto equivocado, es puro peso.
Ejemplo trabajado: el mismo if en cuatro archivos de Boletia
Vamos a ver el problema en el código real de Boletia. Eres la persona nueva del equipo, ya recorriste el sistema en el módulo 1 y ya desarmaste el condicional de precios en el módulo 3. Hoy te llega un encargo simple en apariencia:
"Vamos a agregar PayPal como cuarta forma de pago. ¿Puedes hacerlo?"
Empiezas por donde empezaría cualquiera: el checkout, que es donde se cobra.
# Archivo: checkout/checkout.py
# El corazón del sistema. Aquí se cobra la orden.
def charge_order(order):
"""Cobra una orden con el proveedor que el cliente eligió."""
# La decisión de QUÉ proveedor usar vive aquí dentro, mezclada con el cobro.
if order.provider == "stripe":
client = StripeClient(api_key=settings.STRIPE_KEY)
# Stripe trabaja en centavos y como entero, por eso el int() y el *100.
return client.create_charge(amount=int(order.total * 100), currency="MXN")
elif order.provider == "mercadopago":
client = MercadoPagoClient(token=settings.MP_TOKEN)
# MercadoPago quiere un float y el concepto como texto aparte.
return client.pay(order.total, description=f"Boletia #{order.id}")
elif order.provider == "cash":
# El pago en efectivo no cobra nada: genera una referencia para pagar en tienda.
return {"status": "pending", "reference": generate_cash_reference(order.id)}
else:
raise ValueError(f"Proveedor desconocido: {order.provider}")
Bien. Agregas tu rama de PayPal, corres las pruebas, pasan. Te sientes listo. Y entonces alguien del equipo te dice: "ojo, también hay que tocar el endpoint". Vas a api/routes.py:
# Archivo: api/routes.py
# Recibe la petición HTTP y arma la Order antes de mandarla al checkout.
VALID_PROVIDERS = ["stripe", "mercadopago", "cash"] # ← el mismo conocimiento, otra forma
def post_checkout(request):
data = request.json
provider = data.get("provider")
# Validamos antes de crear la orden, para devolver un 400 limpio
# en vez de reventar más adentro con un ValueError.
if provider not in VALID_PROVIDERS:
return response(400, {"error": f"Proveedor no soportado: {provider}"})
# Algunos proveedores exigen datos extra en la petición. Otro if sobre lo mismo.
if provider == "stripe" and not data.get("card_token"):
return response(400, {"error": "Falta card_token para pagos con tarjeta"})
if provider == "cash" and not data.get("customer_email"):
return response(400, {"error": "El pago en efectivo requiere correo para enviar la referencia"})
order = build_order(data)
return checkout(order)
Ahí está otra vez la lista de proveedores, escrita de otra manera. Agregas "paypal" a VALID_PROVIDERS y su validación. Dos archivos.
Sigues buscando —ya con desconfianza— y encuentras el tercero, en la parte de administración:
# Archivo: admin/refunds.py
# Cuando se cancela un evento hay que devolver el dinero de todas las órdenes pagadas.
def refund_order(order, amount):
"""Devuelve dinero al cliente. Cada proveedor reembolsa a su manera."""
if order.provider == "stripe":
client = StripeClient(api_key=settings.STRIPE_KEY)
return client.create_refund(charge_id=order.external_id, amount=int(amount * 100))
elif order.provider == "mercadopago":
client = MercadoPagoClient(token=settings.MP_TOKEN)
return client.refund(payment_id=order.external_id, value=amount)
elif order.provider == "cash":
# En efectivo no hay nada que devolver por API: se agenda una devolución manual
# y alguien del equipo la ejecuta en ventanilla.
return schedule_manual_refund(order.id, amount)
else:
raise ValueError(f"No sé reembolsar con: {order.provider}")
Y el cuarto, en el proceso nocturno de conciliación, que compara lo que Boletia cree que cobró contra lo que el proveedor dice que cobró:
# Archivo: reports/reconciliation.py
# Corre cada noche. Le pide a cada proveedor el listado de cobros del día
# y lo compara contra las órdenes marcadas como "paid" en nuestra base.
def fetch_daily_charges(provider_name, date):
if provider_name == "stripe":
client = StripeClient(api_key=settings.STRIPE_KEY)
return client.list_charges(created_after=date)
elif provider_name == "mercadopago":
client = MercadoPagoClient(token=settings.MP_TOKEN)
return client.search_payments(begin_date=date)
elif provider_name == "cash":
# Los pagos en efectivo los reporta la cadena de tiendas en un CSV,
# no hay API. Lo leemos del bucket donde lo dejan cada madrugada.
return read_cash_report_csv(date)
else:
raise ValueError(f"Sin conciliación para: {provider_name}")
Cuatro archivos. Cuatro veces la misma lista de proveedores, escrita de cuatro formas distintas. Tu encargo de "agregar PayPal" pasó de una hora a un día, y la parte difícil no fue escribir el código de PayPal: fue encontrar todos los lugares.
Qué esperar de esto. Lo primero que quiero que notes es que ninguno de esos cuatro archivos está mal escrito. Están claros, tienen comentarios, hacen una cosa cada uno y las pruebas pasan. Si revisaras cualquiera de ellos por separado en un pull request, no tendrías nada que objetar. El problema no es visible desde un solo archivo: solo aparece cuando los pones juntos. Por eso este tipo de deuda técnica sobrevive años en sistemas cuidados por gente competente.
Lo segundo: fíjate en qué es exactamente lo que se repite. No es el código —el código de cada archivo es distinto, uno cobra, otro valida, otro reembolsa, otro consulta—. Lo que se repite es el conocimiento de qué proveedores existen. Eso tiene un nombre en la literatura, y es una de las formas de duplicación más difíciles de detectar, porque no la encuentra ningún detector de código copiado. Un elif provider == "stripe" y un provider not in VALID_PROVIDERS no se parecen en nada como texto, pero saben lo mismo.
Lo tercero, y es lo que convierte esto en un problema de creación y no de comportamiento: mira qué hace cada rama en su primera línea. StripeClient(api_key=settings.STRIPE_KEY). MercadoPagoClient(token=settings.MP_TOKEN). Cada archivo construye el cliente del proveedor. Cada archivo sabe qué clase instanciar, con qué parámetros, y de dónde sale la credencial. Si mañana Stripe exige un parámetro nuevo en su constructor —una versión de API, un timeout, un identificador de cuenta—, tienes que tocar tres archivos que no tienen nada que ver entre sí. En el módulo 7 le vas a poner nombre a este olor: se llama shotgun surgery, cirugía de escopeta, un cambio conceptual que obliga a disparar sobre muchos archivos.
Y lo cuarto, que es la señal más útil: esos cuatro else: raise ValueError son cuatro mensajes de error distintos para el mismo problema. Cuando veas que un mismo error se redacta de cuatro formas en un sistema, casi siempre tienes una decisión repartida.
Los tres problemas que tiene la creación dispersa
Vale la pena separar el problema en sus partes, porque cada parte la resuelve un patrón distinto de este módulo y confundirlas es la forma más común de aplicar el patrón equivocado.
Primer problema: la decisión de qué construir está repartida. Es el que acabas de ver. El sistema tiene cuatro lugares que saben qué implementaciones de PaymentProvider existen. La consecuencia concreta: agregar una implementación toca N archivos, y N crece con el sistema. La solución es concentrar esa decisión en un solo lugar, y ese lugar tiene nombre: Factory (lecciones 2 y 3).
Segundo problema: construir el objeto es complicado en sí mismo. Este es distinto y a veces aparece solo. Piensa en una Order de Boletia: tiene boletos —que se agregan de a uno y cada uno cambia el total—, puede llevar un descuento por cupón, puede llevar datos de facturación si el cliente pidió factura, puede llevar una nota del organizador, y algunos de esos campos dependen de otros. Un constructor que reciba todo eso de golpe termina con diez parámetros y llamadas ilegibles como Order(12, [4, 7], None, None, True, None, "MXN"). Aquí la decisión de qué construir es obvia —una Order, no hay opciones—; lo difícil es cómo armarla. Eso lo resuelve Builder (lección 4), con la advertencia importante de que Python trae herramientas que hacen innecesario el Builder en la mayoría de los casos.
Tercer problema: el objeto construye por dentro lo que necesita. Mira otra vez charge_order(order). Recibe una orden y, adentro, se fabrica su propio cliente de Stripe con la credencial real. ¿Qué implica eso? Que no puedes probar charge_order sin salir a internet a cobrarle a alguien. La función no tiene ninguna forma de recibir un proveedor falso, porque no recibe proveedores: los fabrica. Su firma —charge_order(order)— miente sobre lo que necesita para funcionar. Dice que necesita una orden; en realidad necesita una orden, dos credenciales y conexión a internet. Eso lo resuelve la inyección de dependencias (lección 6), que es una idea mucho más simple que su nombre.
Estos tres problemas se confunden todo el tiempo. Alguien lee sobre Factory, ve un constructor de diez parámetros y mete una Factory que no arregla nada porque el problema era de construcción, no de decisión. Alguien más quiere probar su código, no entiende de dónde viene el acoplamiento, y mete un Singleton para "compartir la configuración" —empeorando exactamente lo que quería arreglar—. La pregunta que te ordena el módulo es corta:
¿Mi problema es qué construir, cómo construirlo, o quién lo construye?
Tres preguntas, tres respuestas: Factory, Builder, inyección. Y si ninguna de las tres te aprieta de verdad, la respuesta es la lección 7: un constructor común y a seguir trabajando.
De dónde viene el módulo 3, y a dónde va este
Un detalle de continuidad que conviene tener claro, porque este módulo se apoya en el anterior de una forma muy concreta.
En el módulo 3 convertiste las reglas de precio de Boletia en una Strategy. Antes había un condicional gigante dentro del cálculo del total; después hubo un contrato PricingRule y cuatro implementaciones —general, VIP, early-bird, cortesía—, cada una en su archivo, cada una probable sola. Ese refactor resolvió el problema de comportamiento: el cómo se calcula un precio dejó de estar mezclado con todo lo demás.
Pero fíjate en lo que ese refactor dejó abierto. Si ahora hay cuatro PricingRule intercambiables, en algún lugar del sistema alguien tiene que mirar un ticket.kind y decir "para este boleto, esta regla". Esa línea existe. Y si nadie la puso en un lugar deliberado, hoy está repartida.
Esta es la relación que casi nunca se enseña y que quiero dejarte instalada desde ahora:
| El patrón | Qué resuelve | Qué deja abierto |
|---|---|---|
| Strategy (módulo 3) | Que haya varias formas intercambiables de hacer algo, sin condicionales dentro del que las usa | Quién decide cuál de esas formas se usa en cada caso |
| Factory (este módulo) | Concentrar esa decisión en un solo lugar, para que quien la usa no sepa qué implementaciones existen | Nada nuevo, pero solo tiene sentido si ya hay implementaciones intercambiables |
Léelo al revés y queda más claro todavía: una Factory sin nada intercambiable que fabricar no sirve de nada, y una Strategy sin un lugar que decida termina con la decisión repartida. Los dos patrones se necesitan. En los libros aparecen en capítulos distintos —uno en "comportamiento", otro en "creación"— y eso genera la ilusión de que se usan por separado. En código real casi siempre van juntos. La lección 3 muestra el par completo en funcionamiento sobre PaymentProvider.
Este módulo también prepara el siguiente. Cuando en la lección 3 centralices la creación de los proveedores, vas a notar algo incómodo: la factory funciona, pero cada proveedor sigue teniendo una interfaz distinta —uno recibe centavos enteros, otro un float, el tercero ni siquiera cobra—. Concentrar la decisión no arregló que las APIs sean incompatibles entre sí. Ese problema es de otra familia y se llama Adapter, y es el módulo 5.
El mapa del módulo
Antes de entrar, aquí está lo que cada lección te deja y —esto es lo importante— con cuánta confianza deberías usarlo.
| Lección | Patrón o idea | Cuándo lo vas a usar de verdad |
|---|---|---|
| 2 | Factory (la forma útil: una función) | Seguido. Es de los patrones con mejor relación valor/costo del catálogo, siempre que ya existan varias implementaciones |
| 3 | Factory aplicada a PaymentProvider, combinada con Strategy | El caso completo, con el antes y el después medidos en archivos tocados |
| 4 | Builder | Poco. En Python, argumentos por nombre y dataclass cubren casi todos los casos; el Builder gana cuando hay pasos con lógica o validación del conjunto |
| 5 | Singleton | Nunca a propósito. Se estudia para reconocerlo en código ajeno y saber por qué duele |
| 6 | Inyección de dependencias | Todo el tiempo, y sin llamarla así. Es la idea más útil del módulo y la más simple |
| 7 | El constructor común | La mayoría de las veces. Esta es la vacuna del módulo |
| 8 | Proyecto | Ordenar PaymentProvider y NotificationChannel con el patrón mínimo, y escribir qué no abstrajiste |
Nota la forma de esa tabla, porque no es la que traen los libros. De los cuatro patrones del módulo, uno se usa seguido (Factory), uno se usa poco (Builder), uno no se usa nunca a propósito (Singleton) y la idea más valiosa —la inyección de dependencias— ni siquiera es un patrón del catálogo original: es una forma de escribir funciones. Si sales de este módulo con una sola cosa, que sea esa.
Y sobre Singleton, para que no haya duda desde la primera lección: está en el temario para que aprendas a reconocerlo, no para que lo apliques. Vas a encontrarlo en código heredado, en respuestas de foros de hace quince años y en más de una entrevista. Vas a necesitar saber qué problema creía resolver, por qué la industria se movió en contra, y qué se usa hoy en su lugar. Lo que no vas a necesitar es escribir uno.
Errores comunes
Confundir "creación dispersa" con "código duplicado" (conceptual). Qué pasa: alguien ve los cuatro archivos de Boletia, reconoce que hay repetición y ataca el síntoma equivocado —extrae una función auxiliar que arma el StripeClient, otra para el de MercadoPago, y las llama desde los cuatro lugares—. El código queda un poco más corto, pero los cuatro archivos siguen sabiendo qué proveedores existen y agregar PayPal sigue tocando cuatro lugares. Por qué pasa: la duplicación de código es visible y la duplicación de conocimiento no. Uno ve dos bloques parecidos y el reflejo entrenado es "extrae una función". Cómo detectarlo: pregúntate qué pasa si mañana entra un proveedor nuevo. Si la respuesta sigue siendo "toco cuatro archivos", no resolviste el problema, solo lo peinaste. Cómo corregirlo: la prueba concreta es la de la lección 3 —contar archivos tocados antes y después—. Si el número no bajó, el refactor no sirvió.
Meter una Factory donde el problema era de construcción, no de decisión (de criterio). Qué pasa: alguien tiene un constructor con nueve parámetros, le molesta con razón, lee sobre patrones creacionales y escribe una OrderFactory. La factory queda con los mismos nueve parámetros más una capa de indirección encima, y ahora hay dos lugares que hay que entender en vez de uno. Por qué pasa: los patrones creacionales se presentan como una familia, y "familia" invita a pensar que son intercambiables. No lo son: resuelven problemas distintos. Factory resuelve qué construir cuando hay varias opciones; si solo hay una opción, no hay nada que decidir y la factory es una función que llama a un constructor. Cómo detectarlo: si tu factory tiene un solo camino posible —siempre devuelve el mismo tipo—, no es una factory, es un constructor con sombrero. Cómo corregirlo: vuelve a la pregunta de tres ramas. Con una sola clase posible y construcción complicada, el candidato es Builder o —más probable en Python— argumentos por nombre con valores por defecto. La lección 4 lo desarrolla.
Creer que este módulo trata sobre "objetos" y no aplica si trabajas con funciones (de expectativa). Qué pasa: alguien que escribe Python mayormente funcional —o que viene de un lenguaje sin clases fuertes— asume que los patrones creacionales son cosa de la programación orientada a objetos y se salta el módulo. Después escribe, sin darse cuenta, cuatro funciones que arman el mismo cliente de una API en cuatro archivos distintos. Por qué pasa: el catálogo original está escrito en el vocabulario de la orientación a objetos de 1994 y usa la palabra "instanciar" en cada página. Eso hace pensar que sin class no aplica. Cómo detectarlo: si en tu código hay más de un lugar que decide cuál de varias cosas usar según un dato —una función, un módulo, un cliente, una configuración—, el problema es exactamente el mismo, aunque no haya una sola clase involucrada. Cómo corregirlo: traduce el vocabulario. Donde el libro dice "instancia", lee "la cosa que vas a usar". Donde dice "clase", lee "la forma de crearla". La Factory de este módulo va a ser, en su versión recomendada, una función que devuelve otra cosa —nada más orientado a objetos que eso—.
Ejercicios
Ejercicio 1 — Cuenta los lugares en tu propio código. Piensa en un sistema en el que hayas trabajado —de trabajo, de escuela o personal—. Busca un caso donde el código elija entre varias opciones según un dato: un tipo de usuario, un formato de archivo, un entorno (dev/prod), un idioma, un método de envío. Anota: (a) cuántos lugares distintos toman esa decisión, (b) qué tendrías que tocar hoy para agregar una opción más, y (c) si alguna vez alguien olvidó uno de esos lugares y qué pasó.
Ver solución
No hay respuesta única porque el sistema es tuyo, pero el resultado suele tener una forma reconocible y vale la pena que la veas en tu propio material.
Sobre (a): lo más común es descubrir que son más de los que recordabas. La decisión suele empezar en un lugar legítimo —el que de verdad ejecuta la acción— y después se filtra a los bordes: una validación en la entrada, un mensaje distinto en la interfaz, una rama en el reporte, un caso especial en el proceso nocturno. Ninguno de esos se agregó por descuido; cada uno tenía su razón el día que se escribió.
Sobre (b): si tu respuesta a "qué tocarías para agregar una opción" es una lista y no un archivo, ya tienes el diagnóstico completo. Y hay un detalle revelador: normalmente la gente puede nombrar dos o tres lugares de memoria, pero no está segura de que sean todos. Esa inseguridad —"creo que ya están todos"— es en sí misma el costo del problema.
Sobre (c): si el olvido ya ocurrió, fíjate en cómo se descubrió. Casi nunca lo encuentra una prueba; casi siempre lo encuentra un usuario o un reporte que no cuadra. Eso pasa porque cada lugar por separado funciona bien, y las pruebas de cada lugar también. Lo que falla es la coherencia entre ellos, y eso rara vez tiene una prueba que lo cubra.
Por qué funciona: el módulo entero se apoya en que reconozcas este problema en tu propio terreno, no solo en Boletia. Los patrones que vienen son la respuesta; sin haber sentido la pregunta, se estudian como trivia.
Ejercicio 2 — Clasifica cinco situaciones en los tres problemas. Para cada situación, di si el problema es de decisión (qué construir), de construcción (cómo armarlo) o de quién construye (el objeto se fabrica sus dependencias por dentro). Justifica en una línea.
(a) Una función send_notification(customer, message) que adentro crea un SmtpClient con el servidor y la contraseña reales, y por eso ninguna prueba puede correr sin mandar correos de verdad.
(b) Order(customer_id, ticket_ids, total, discount, billing_name, billing_tax_id, note, currency, created_at) — nueve parámetros, cinco de ellos casi siempre None.
(c) Tres archivos distintos que hacen if kind == "csv" ... elif kind == "pdf" ... elif kind == "xlsx" para elegir el exportador de reportes.
(d) Una clase EventCache con un atributo _instance y un método get_instance() que todo el sistema llama.
(e) Un ReportExporter que necesita, para construirse, una conexión a la base, una plantilla, una zona horaria y una lista de columnas —y ninguna de esas cuatro cosas tiene un valor por defecto razonable.
Ver solución
(a) Quién construye. La función fabrica su propia dependencia con credenciales reales, así que quien la llama no tiene forma de sustituirla. El síntoma clásico: la firma dice que necesita un cliente y un mensaje, pero en realidad necesita también un servidor de correo. Se resuelve pasándole el cliente como parámetro —lección 6—.
(b) Construcción. No hay ninguna decisión que tomar: siempre se construye una Order, no hay opciones. Lo difícil es armarla de forma legible. Candidato a Builder, aunque la lección 4 va a mostrar que en Python un dataclass con valores por defecto probablemente ya alcanza.
(c) Decisión. Es el caso de Boletia con otro sombrero: el conocimiento de qué exportadores existen está repartido en tres lugares. Factory.
(d) Quién construye, disfrazado de otra cosa. Un get_instance() es un punto de acceso global: cualquier parte del sistema puede tomar el caché sin declararlo, y por eso nadie sabe quién lo usa. Es el Singleton de la lección 5, y su reemplazo es —otra vez— pasarlo como parámetro.
(e) Este es el interesante y por eso lo puse al final. Es de construcción, pero probablemente sea el síntoma de otro problema. Cuatro dependencias obligatorias y sin defaults no piden un Builder: piden preguntarse si el ReportExporter no está haciendo demasiado. Un objeto difícil de construir suele ser un objeto que sabe demasiado. Antes de aplicar un patrón de creación, revisa si el problema es de diseño de responsabilidades —módulo 7, el olor se llama God object—.
Por qué funciona: la mayoría de los errores de este módulo son de diagnóstico, no de implementación. Si clasificas bien el problema, elegir el patrón es casi automático; si lo clasificas mal, la implementación perfecta del patrón equivocado no arregla nada.
Ejercicio 3 — Predice el costo de agregar PayPal. Con los cuatro archivos de Boletia que viste en el ejemplo trabajado, escribe la lista concreta de cambios que hay que hacer hoy para soportar PayPal. Después responde: ¿cuál de esos cambios es el más fácil de olvidar, y por qué? ¿Y cuál rompería el sistema de forma silenciosa, sin lanzar ningún error?
Ver solución
La lista de cambios de hoy:
checkout/checkout.py— una ramaelif order.provider == "paypal"que construya el cliente y cobre.api/routes.py— agregar"paypal"aVALID_PROVIDERS, y si PayPal exige algún dato extra, su validación correspondiente.admin/refunds.py— una rama que sepa reembolsar con PayPal.reports/reconciliation.py— una rama que sepa pedirle a PayPal el listado de cobros del día.
Más los que no se ven en el código: la credencial nueva en settings, el despliegue con esa credencial, y probablemente algo en la interfaz para que el botón exista.
El más fácil de olvidar es el cuarto, la conciliación. Por dos razones que se refuerzan: vive en un archivo que nadie abre porque corre solo de madrugada, y no aparece en ninguna búsqueda obvia —quien busca "stripe" lo encuentra, pero quien busca "checkout" o "pago" no—. Este es el punto: la decisión repartida no solo se repite, se esconde en rincones con distinta frecuencia de visita.
El que rompe en silencio es también ese cuarto. Piénsalo: si olvidas la rama del checkout, el primer cliente que intente pagar con PayPal recibe un ValueError y alguien se entera en minutos. Si olvidas la validación del endpoint, el error aparece más adentro pero aparece. Pero si olvidas la conciliación, el proceso nocturno simplemente no encuentra los cobros de PayPal: las órdenes están pagadas en la base, el dinero entró de verdad, y el reporte dice que no cuadra —o peor, según cómo esté escrito el else, ni siquiera dice nada—. Ese tipo de falla se descubre semanas después, cuando alguien de finanzas pregunta por qué los números no coinciden.
Y esa es la razón profunda por la que este módulo existe. El costo de la creación dispersa no es escribir cuatro ramas en vez de una: escribir código es barato. El costo es que el sistema no tiene forma de decirte que te faltó una. Cuando la decisión está en un solo lugar, olvidarla es imposible: o el proveedor está registrado o no está, y si no está, falla en todos lados de la misma manera y de inmediato.
Por qué funciona: acabas de valuar el refactor antes de hacerlo, que es exactamente lo que el módulo 2 te pidió aprender a hacer. La factory de la lección 3 no se justifica porque sea elegante; se justifica porque convierte una falla silenciosa en una falla ruidosa.
Resumen y siguiente paso
En esta lección instalaste el problema que ordena todo el módulo: la decisión de qué objeto construir tiende a repartirse por el sistema y a duplicarse, y esa duplicación es de conocimiento, no de código, por lo que ninguna herramienta la detecta y ninguna prueba la cubre. Lo viste en Boletia con nombre y apellido: el mismo conocimiento de qué proveedores de pago existen, escrito de cuatro formas distintas en checkout/checkout.py, api/routes.py, admin/refunds.py y reports/reconciliation.py. Y viste que el costo real no es escribir la cuarta rama —eso es barato— sino que el sistema no tiene forma de avisarte cuando te falta una.
Separaste el problema en sus tres partes, que es lo que evita aplicar el patrón equivocado: si el problema es qué construir, la respuesta es Factory; si es cómo construirlo, la respuesta es Builder —o, casi siempre en Python, algo más simple—; si es quién lo construye, la respuesta es la inyección de dependencias. Y viste la continuidad con el módulo 3: la Strategy define piezas intercambiables y deja abierta la pregunta de quién elige; la Factory contesta esa pregunta. Se usan juntas, aunque los libros las separen.
Antes de avanzar deberías poder: explicar por qué cuatro archivos con el mismo if son un problema aunque cada uno esté bien escrito; distinguir las tres preguntas del módulo en un caso concreto; y decir por qué Singleton está en el temario a pesar de que no vas a escribir uno.
La lección 2 entra al primero de los tres. Vamos a definir Factory en su forma útil —que es mucho más modesta y mucho más práctica que la del catálogo—, a desarmar su anatomía en cuatro piezas, y a dejar claro por qué en Python una función suele bastar donde el libro pide una jerarquía de clases. Y, en la línea del módulo 2, vas a salir sabiendo cuándo una Factory no se gana su lugar.
Recursos
- Refactoring Guru — Creational Patterns — la vista general de la familia con diagramas claros. Útil como mapa; recuerda que presenta las variantes ceremoniales como si fueran el caso normal.
- Martin Fowler — Inversion of Control Containers and the Dependency Injection pattern — el artículo que puso orden en el vocabulario de la inyección de dependencias. Es de 2004 y sigue siendo la mejor explicación del tema; la lección 6 se apoya en él.
- Refactoring — Catalog: Replace Constructor with Factory Function — el refactor concreto que vas a aplicar en la lección 3, descrito paso a paso por Fowler.
- Python Design Patterns — python-patterns.guide — Brandon Rhodes revisa el catálogo desde Python y dice sin rodeos cuáles sobran en este lenguaje. Su capítulo sobre el Singleton es la mejor lectura complementaria de la lección 5.