Módulo 4: Patrones para crear objetos
7. Cuándo un constructor o una función alcanzan
Descripción
Al terminar esta lección vas a tener el antídoto contra lo que acabas de aprender. Después de cinco lecciones enseñando Factory, Builder e inyección de dependencias, toca decir en voz alta lo que el módulo 2 ya había instalado: la inmensa mayoría de los objetos de un sistema no necesitan nada de esto. Se construyen con un constructor, se usan, y ya. Vas a salir con las tres señales concretas de que un patrón de creación sí hace falta, con las tres señales falsas que engañan a todo el mundo, y con una forma de medir el costo de la ceremonia que puedes usar en una revisión de código sin que suene a opinión.
Esto importa por una razón que ya conoces de nombre y que ahora te toca de cerca. En el módulo 1 hablamos del síndrome del martillo nuevo: quien acaba de aprender un patrón ve el problema que ese patrón resuelve en todos lados. Tú estás en ese momento exacto. Acabas de pasar cinco lecciones viendo cómo una Factory ordena un desastre de cuatro archivos, cómo un Builder domestica un constructor de once parámetros y cómo la inyección hace probable lo improbable. Los tres funcionaron. Y esa es justamente la condición mental en la que se escribe código sobre-diseñado: no por descuido, sino por entusiasmo bien fundado.
Esta lección no viene a decirte que lo anterior estaba mal. Viene a ponerle un filtro adelante. Y el filtro es más útil que los patrones, porque los patrones los puedes buscar cuando los necesites y el criterio no.
Conexión con el módulo: esta es la vacuna, y va deliberadamente después de las tres técnicas y antes del proyecto. Las lecciones 2 y 3 te dieron Factory, la 4 Builder, la 6 la inyección; esta pone las tres bajo la misma pregunta: ¿esto se gana su lugar?. Es la aplicación directa del módulo 2 —cada abstracción se paga, la regla de tres, YAGNI— al terreno específico de la creación de objetos. Y prepara el proyecto de la lección 8, que te va a pedir explícitamente ordenar la creación dispersa con el patrón mínimo y entregar por escrito qué decidiste no abstraer. Sin esta lección, ese proyecto se resuelve metiendo tres patrones. Con esta, se resuelve pensando.
El destornillador eléctrico para colgar un cuadro
Alguien te regala un juego de herramientas. Trae taladro percutor, destornillador eléctrico con dieciocho puntas, nivel láser y una caja de tornillos de expansión. Es un buen regalo y las herramientas son buenas.
Al día siguiente quieres colgar un cuadro pequeño en una pared de tabla-roca. La forma correcta de hacer eso es con un clavito y tres golpes de martillo: treinta segundos, y si te equivocas de altura, lo sacas y vuelves a empezar.
Pero tienes el juego nuevo. Así que sacas el taladro, buscas la broca, marcas con el nivel láser, taladras, metes el taquete, atornillas. Veinte minutos. El cuadro queda colgado —igual de bien que con el clavito— y ahora hay un agujero de ocho milímetros con un taquete de plástico dentro. Si mañana quieres mover el cuadro, ese agujero se queda ahí para siempre.
Fíjate en las tres cosas que costó, porque son exactamente las tres que cuesta un patrón innecesario:
Costó tiempo hoy. Veinte minutos contra treinta segundos.
Costó reversibilidad. El clavito se saca y no queda nada. El taquete queda. En código: una función se borra en un minuto; una jerarquía de clases que ya tiene tres usuarios no la toca nadie.
Costó una expectativa falsa. Quien vea ese anclaje va a suponer que ahí colgaba algo pesado y va a tratarlo con respeto. En código: quien vea una NotifierFactory va a suponer que hay varios notificadores y va a buscarlos.
Ese tercer costo es el que más se subestima y el que más caro sale a la larga. Una abstracción es una promesa. Cuando escribes una factory, estás comunicando "hay varias implementaciones y esto elige entre ellas". Si hay una sola, mentiste sin querer, y el siguiente que llegue va a perder media hora buscando lo que prometiste.
Lo que se construye y ya
Vamos a mirar Boletia con honestidad. ¿Cuántos de sus objetos necesitan un patrón de creación?
# Estos se construyen y punto. No hay nada que decidir, nada que armar por pasos,
# nada que sustituir en pruebas.
ticket = Ticket(event_id=9, kind="vip", base_price=1200.0, seat="A-14")
money = Money(amount=1740.0, currency="MXN")
customer = Customer(id=142, name="Ana Ruiz", email="ana@example.com")
result = PaymentResult(status="succeeded", external_id="ch_1a2b3c")
fee = ProviderFee(percentage=0.036, fixed=3.0)
billing = BillingInfo(name="Ana Ruiz", tax_id="RUAN850312AB1", address="Reforma 222")
date_range = DateRange(start=date(2026, 3, 1), end=date(2026, 3, 31))
Siete objetos. Ninguno necesita nada. Y no son casos menores: Ticket y Order son el corazón del dominio de Boletia. El corazón de un sistema casi siempre se construye con constructores comunes.
¿Por qué? Porque los patrones de creación resuelven problemas que aparecen en las fronteras: donde hay varias implementaciones intercambiables, donde se habla con lo externo, donde entra la configuración. El centro de un sistema —los datos del negocio, las reglas puras— rara vez tiene esos problemas.
Esa es la primera observación que quiero dejarte, porque reordena el mapa: los patrones de creación viven en el borde del sistema, no en su núcleo. Si estás aplicando uno en el corazón del dominio, revisa dos veces.
Las tres señales de que sí hace falta
Ahora el filtro. Un patrón de creación se gana su lugar cuando aparece al menos una de estas tres cosas. No dos de tres, no "más o menos": una, claramente.
Señal 1 — La misma decisión se toma en más de un lugar
Esta es la señal de la Factory, y es la más objetiva de las tres porque se puede contar.
No se trata de que exista un condicional. Se trata de que el mismo condicional —el mismo conocimiento sobre qué opciones existen— aparezca en varios archivos. En Boletia eran cuatro lugares, escritos de cuatro formas distintas.
La prueba concreta, la que puedes correr sobre cualquier código:
¿Cuántos archivos tengo que abrir para agregar una opción nueva?
Uno: no hay problema que resolver. Tres o más: tienes el problema, y la Factory lo resuelve.
Y presta atención al matiz que ya viste en el ejercicio 1 de la lección 2, porque es donde más gente se equivoca: tres implementaciones en un solo lugar de decisión no piden Factory. La regla de tres del módulo 2 habla de cuántas implementaciones hay; esta señal habla de cuántos lugares deciden. Son dos números distintos y hay que mirar el segundo.
Señal 2 — La construcción es compleja de verdad
Esta es la señal del Builder, y "compleja" tiene una definición precisa, no un umbral de campos.
No es "tiene muchos parámetros" —eso lo resuelve un dataclass con argumentos por nombre, como viste en la lección 4—. Es una de estas tres:
- Los datos llegan en momentos distintos. El carrito de Boletia, el borrador de evento que se guarda a medias.
- Agregar cada pieza requiere trabajo.
add_ticketconsulta la regla de precio, calcula y acumula. No es una asignación. - Hay reglas que solo se pueden verificar con el objeto completo. "Una orden de solo cortesías no admite cupón" necesita ver los boletos y el descuento a la vez.
La prueba concreta:
¿Mis pasos hacen algo, o solo guardan?
Si todos son self._x = x, no hay complejidad de construcción: hay muchos campos. Y muchos campos los resuelve el lenguaje.
Señal 3 — Necesitas sustituir la pieza
Esta es la señal de la inyección de dependencias, y es la más frecuente de las tres.
"Sustituir" significa dos cosas concretas: en pruebas, poder pasar un doble; en producción, poder pasar una implementación distinta según el contexto —otro proveedor, otro país, otro entorno—.
La prueba concreta, y es de una sola pregunta:
¿Alguna vez le voy a pasar algo distinto?
Si la respuesta honesta es "nunca", ese parámetro es ruido en la firma. Si es "sí, en las pruebas", ya está: se inyecta.
Y el atajo que resume esta señal: se inyecta lo que cruza una frontera —red, disco, base de datos, reloj, azar, procesos externos— y lo que tiene más de una implementación real. Lo puro y lo transversal, no.
Las tres señales falsas
Ahora las que engañan. Estas tres se sienten como razones y no lo son.
Falsa 1 — "Hay un if"
Un condicional no es un olor. Un condicional es cómo se escribe una decisión, y los programas toman decisiones.
# Esto está PERFECTO. No lo toques.
def format_seat(ticket) -> str:
if ticket.seat is None:
return "Admisión general"
return f"Fila {ticket.seat[0]}, asiento {ticket.seat[1:]}"
Dos ramas, no va a crecer —un boleto tiene asiento o no lo tiene, no hay un tercer caso posible—, vive en un solo lugar. Convertir esto en una jerarquía SeatFormatter con dos implementaciones y una factory sería tres archivos para reemplazar cuatro líneas.
Es la misma vacuna que el módulo 3 ya te dio en su lección 7 —no todo condicional es una Strategy escondida— aplicada a la creación. Lo que importa no es que haya un if: es si el if crece y si está repetido.
Falsa 2 — "Algún día vamos a necesitar otro"
Esta es YAGNI, y es la señal falsa que produce el código más caro.
El problema no es que la predicción sea incorrecta —a veces acierta—. El problema es que abstraer hoy te obliga a elegir el eje de variación con la información de hoy, que es la peor información que vas a tener nunca sobre ese problema.
Un ejemplo real de Boletia. Hace dos años alguien pensó: "algún día vamos a guardar los PDF en la nube". Escribió una interfaz Storage con save(path, data) y una implementación local. Un año después llegó el almacenamiento en la nube, y resultó que la diferencia importante no era dónde se guarda: era que la nube devuelve URLs firmadas con expiración, que los archivos grandes necesitan subida por partes, y que borrar cuesta dinero. La interfaz que se escribió no contemplaba nada de eso, así que hubo que rehacerla —y ahora había cinco lugares usándola—. La abstracción prematura no ahorró trabajo: lo duplicó, y de paso hizo que el segundo trabajo fuera más riesgoso.
La regla del módulo 2 aplicada aquí: espera al segundo caso real, y preferentemente al tercero. Para entonces vas a saber cuál es el eje de verdad.
Falsa 3 — "Así se ve más profesional"
Esta nadie la dice en voz alta, y es la más común.
Un archivo con una interfaz, dos implementaciones y una factory se ve como código de una empresa grande. Un archivo con una función de doce líneas se ve como un script. Y hay una presión real —en revisiones de código, en entrevistas, en la propia cabeza— hacia lo primero.
Vale la pena decirlo sin rodeos: en un equipo maduro, el código simple que resuelve el problema se valora más que el código estructurado que resuelve el mismo problema con tres archivos. La habilidad que se nota no es aplicar patrones: es saber cuándo no. Cualquiera puede agregar una capa; quitarla requiere entender el problema.
Y hay una versión de esta señal falsa que es más difícil de detectar porque viene disfrazada de humildad: "lo hago así porque es como lo hacen los ejemplos". Los ejemplos de patrones están escritos para mostrar el patrón. Su contexto es "aquí hay un caso donde esto aplica", no "así se escribe todo".
Ejemplo trabajado: cinco decisiones sobre el mismo código
Vamos a aplicar el filtro a cinco rincones de Boletia. En cada uno, el reflejo entrenado dice "patrón" y la respuesta correcta no siempre es esa.
Caso 1 — El validador de cupones.
def validate_coupon(code: str, subtotal: float) -> Coupon:
coupon = find_coupon(code)
if coupon is None:
raise UnknownCouponError(code)
if coupon.expires_at < date.today():
raise ExpiredCouponError(code)
if subtotal < coupon.minimum_purchase:
raise CouponNotApplicableError(code, coupon.minimum_purchase)
return coupon
Señal 1 (¿decisión repetida?) No, se valida en un solo lugar.
Señal 2 (¿construcción compleja?) No construye nada, valida.
Señal 3 (¿hay que sustituir?) Hay dos candidatos: find_coupon toca la base de datos y date.today() es el reloj.
Veredicto: inyectar dos cosas, nada más.
def validate_coupon(code, subtotal, coupons: CouponRepository, today: date) -> Coupon:
...
Fíjate en el today: date en vez de un clock invocable. Es la regla del ejercicio 3 de la lección 6: pasa el valor, no la fuente del valor. Esta función no necesita un reloj; necesita una fecha.
Caso 2 — La generación del código de barras.
def barcode_for(ticket) -> str:
prefix = "BOL"
checksum = sum(ord(c) for c in f"{ticket.event_id}{ticket.id}") % 97
return f"{prefix}-{ticket.event_id:05d}-{ticket.id:07d}-{checksum:02d}"
Las tres señales: no, no y no. Es una función pura, en un lugar, sin dependencias.
Veredicto: se queda exactamente como está. Y esto no es un caso de relleno: es el 70% del código de cualquier sistema. Funciones que hacen una cosa con lo que reciben. La mayor parte de tu trabajo va a ser escribir código así, y está bien que sea así.
Caso 3 — El cálculo de comisiones para el reporte.
# scripts/export_provider_fees.py
def total_fees(orders):
total = 0.0
for order in orders:
if order.provider == "stripe":
total += order.total * 0.036 + 3.0
elif order.provider == "mercadopago":
total += order.total * 0.041
elif order.provider == "cash":
total += 12.0
return total
Señal 1: aquí hay que mirar bien. El condicional está en un solo archivo. Pero el conocimiento —cuánto cobra cada proveedor— es de los proveedores, que ya viven en payments/ con su propia factory. Así que el conocimiento sí está repartido: parte en payments/, parte aquí.
Veredicto: sí, pero no una factory nueva. Es el ejercicio 3 de la lección 3: la tarifa se muda al proveedor como un dato, y este script recorre el registro que ya existe. La respuesta no fue "agrega un patrón" sino "mueve el conocimiento a donde ya vive su dueño". Ese movimiento —cohesión, no indirección— resuelve más problemas de los que resuelve cualquier patrón, y casi nunca se enseña porque no tiene nombre de catálogo.
Caso 4 — El armado del correo de confirmación.
def build_confirmation_email(order, customer, event) -> Email:
return Email(
to=customer.email,
subject=f"Tu compra para {event.name}",
body=render_template("confirmation.html", order=order, customer=customer, event=event),
attachments=[ticket_pdf(t) for t in order.tickets],
reply_to=event.organizer_email,
)
Cinco campos, uno de ellos calculado, otro que es una lista. Alguien mira eso y piensa "Builder".
Señal 1: no, un solo lugar. Señal 2: todos los datos llegan juntos, ningún paso hace trabajo acumulativo —el render_template y los adjuntos se calculan, pero no dependen del estado parcial del correo—. Señal 3: render_template lee del disco y ticket_pdf genera un archivo; los dos son candidatos.
Veredicto: nada de Builder. Un dataclass para Email y, si quieres pruebas rápidas, inyectar el renderizador. Es el caso más común de falso positivo del Builder: muchos campos no es lo mismo que construcción compleja.
Caso 5 — El registro de asistencia en la puerta del evento.
class CheckInService:
def __init__(self):
self._db = Database(settings.DATABASE_URL)
self._scanner = BarcodeScanner(port="/dev/ttyUSB0")
self._printer = WristbandPrinter(ip=settings.PRINTER_IP)
Señal 1: no. Señal 2: no. Señal 3: las tres dependencias cruzan fronteras: base de datos, un puerto de hardware y una impresora en red. Para probar esta clase hoy necesitas un escáner físico conectado.
Veredicto: inyección, las tres, y sin discusión. Es el caso más claro de los cinco. Nota que aquí no hay ningún patrón del catálogo involucrado: solo mover tres líneas del cuerpo del constructor a sus parámetros.
Qué esperar de estos cinco casos. Cuéntalos: de cinco rincones donde el reflejo entrenado grita "patrón", ninguno necesitó una Factory nueva, ninguno necesitó un Builder, dos necesitaron inyección de dependencias, uno necesitó mover conocimiento de lugar, y dos se quedaron exactamente como estaban.
Esa proporción no es un truco del ejemplo: es aproximadamente la que vas a encontrar en el trabajo. La inyección de dependencias es la única de las tres técnicas del módulo que se usa seguido. Factory se usa cuando de verdad hay una decisión repartida, que ocurre pero no todos los días. Builder es raro en Python. Y una parte enorme del código no necesita nada.
Y fíjate en el caso 3, porque es el más instructivo de los cinco. El problema era real, pero la solución no fue agregar estructura: fue mover conocimiento a donde pertenece. Ese movimiento no tiene nombre de patrón y por eso casi nadie lo considera primero. Antes de preguntarte qué patrón aplicar, pregúntate si algo está simplemente en el lugar equivocado.
Cómo medir el costo de la ceremonia
Un problema práctico: en una revisión de código, "creo que esto es demasiado" suena a opinión y no convence a nadie. Necesitas una medida.
Aquí van dos que funcionan.
Medida 1 — Cuenta los saltos de lectura. Toma una pregunta concreta que alguien nuevo haría, y cuenta cuántos archivos hay que abrir para responderla.
"¿Qué pasa exactamente cuando un cliente paga con efectivo?"
- Con el código de la lección 1 (todo en el checkout): un archivo. Fácil de responder, difícil de mantener.
- Con la factory de la lección 3: tres archivos —el checkout, la factory, el proveedor—. Es el precio que pagamos y lo pagamos a sabiendas.
- Con una arquitectura de plugins con descubrimiento dinámico: cinco o seis archivos, y uno de ellos hay que ejecutarlo mentalmente para saber qué se registró. Ese es el rincón
plugins/del módulo 2.
Tres saltos por una ganancia concreta es un buen trato. Seis saltos por nada es el anti-patrón.
Medida 2 — Cuenta las líneas de ceremonia por línea de trabajo. En cualquier abstracción, separa las líneas que hacen algo de las que solo conectan.
# El TicketBuilder de la lección 4:
# 30 líneas de ceremonia (los with_x, el __init__ del builder)
# 4 líneas de trabajo (los cuatro campos que terminan en el Ticket)
# Proporción: 7.5 a 1 → mala señal
# La factory de proveedores de la lección 3:
# 12 líneas de ceremonia (el registro, el error, la función)
# ~90 líneas de trabajo (las tres clases de proveedor, que existen igual)
# Proporción: 1 a 7.5 → buena señal
Cuando la ceremonia le gana al trabajo, la abstracción está pesando más de lo que carga. Es una medida burda, y por eso es útil: se calcula en veinte segundos y se puede poner en un comentario de revisión sin que suene a gusto personal.
La tabla que resume el módulo
Guárdate esta, porque es el módulo entero en una pantalla:
| Tu situación | La respuesta |
|---|---|
| Un objeto de datos con campos, así de simple | Constructor. dataclass si quieres __repr__ y __eq__ gratis |
| Muchos campos, varios opcionales | dataclass con defaults y kw_only=True |
| Lo anterior más reglas que cruzan campos | __post_init__ |
| Una función pura que calcula algo | Nada. Déjala en paz |
Un if de dos ramas que no va a crecer, en un solo lugar | Nada. El if está bien |
El mismo if sobre las mismas opciones, en 3+ lugares | Factory |
| Los datos llegan en momentos distintos, o los pasos hacen trabajo | Builder |
| El objeto habla con red, disco, base de datos, reloj o azar | Inyección de dependencias |
| Quieres una sola instancia de algo caro | Créala en el arranque y pásala. Nunca un Singleton |
| "Algún día vamos a necesitar otro" | Nada, hoy. Espera al segundo caso real |
| El conocimiento está en el archivo equivocado | Muévelo. Ningún patrón |
Nota cuántas filas dicen "nada". Son cinco de once. Esa proporción es el mensaje de la lección.
Errores comunes
Aplicar la vacuna al revés y dejar todo sin estructura (de criterio). Qué pasa: alguien sale de esta lección convencido de que los patrones son sospechosos, y entonces deja pasar los casos donde de verdad hacían falta. El if de proveedores crece a siete ramas en cinco archivos, y cada vez que alguien lo señala, la respuesta es "no abstraigas prematuramente". La sub-estructura tiene un costo tan real como la sobre-estructura; solo es menos visible porque se paga en pequeñas dosis diarias. Por qué pasa: "no abstraigas" es más fácil de recordar que "abstrae cuando se cumplan estas señales", y en la duda la gente se queda con la regla simple. Cómo detectarlo: si tu equipo lleva meses diciendo "hay que arreglar ese if" y nadie lo arregla, ya no estás evitando la abstracción prematura: estás evitando el trabajo. Cómo corregirlo: las señales son un filtro en las dos direcciones. Si se cumple una, claramente, el patrón se gana su lugar y postergarlo también cuesta.
Confundir "simple" con "corto" (conceptual). Qué pasa: alguien evita una factory y termina con una función de 200 líneas con siete condicionales anidados, y la defiende diciendo "es más simple, está todo en un archivo". Está todo en un archivo, sí, pero nadie puede leerlo. Por qué pasa: la crítica a la indirección se traduce mentalmente en "menos archivos es mejor", y eso no es lo que dice. Cómo detectarlo: pregúntale a alguien que no conozca el código que te explique qué hace esa función. Si tarda más de tres minutos o se pierde, no es simple. Cómo corregirlo: la medida no es la cantidad de archivos ni de líneas, es cuánto hay que cargar en la cabeza a la vez para entender una parte. Una función de 200 líneas exige cargarlo todo. Tres archivos de 30 líneas cada uno, con nombres honestos, no. El módulo 2 lo dice con precisión: el tradeoff es entre acoplamiento e indirección, y las dos duelen —la habilidad es elegir cuál duele menos aquí—.
Usar esta lección para rechazar cambios ajenos sin ofrecer nada (de comunicación). Qué pasa: alguien llega a una revisión con "YAGNI" y un enlace, y rechaza una abstracción que su compañero pensó durante dos días. Puede tener razón en el fondo y aun así dejar un equipo peor: la próxima vez esa persona no propone nada. Por qué pasa: las reglas cortas son cómodas de citar y se sienten objetivas. Cómo detectarlo: si tu comentario no menciona el problema concreto que el otro estaba tratando de resolver, estás citando en vez de conversar. Cómo corregirlo: reconoce el problema, aporta el dato que falta —cuántas implementaciones hay hoy, cuántos lugares deciden—, y propón la versión mínima. "Coincido en que el if va a crecer. Hoy hay dos casos y solo este archivo decide; ¿lo dejamos así y extraemos la factory cuando entre el tercero? Si quieres dejo el comentario con la condición para que no se nos pase." Eso es criterio compartido; lo otro es una sentencia. El módulo 7 desarrolla esta diferencia a fondo.
Ejercicios
Ejercicio 1 — Aplica el filtro a cuatro casos. Para cada uno, evalúa las tres señales y da un veredicto: constructor común, dataclass, Factory, Builder o inyección. Justifica con la señal que decidió.
(a) SeatMap: representa el mapa de asientos de un recinto. Tiene filas, secciones y una lista de asientos con su estado. Se construye leyendo un archivo JSON del recinto. Se usa en tres lugares.
(b) Invoice: la factura de una orden. Se arma con los datos fiscales del cliente, los conceptos —un renglón por boleto—, los impuestos y un folio. Todos los datos están disponibles cuando se emite. Hay una regla: el total de los conceptos más impuestos debe cuadrar con el total de la orden.
(c) EventSearchQuery: filtra eventos por ciudad, rango de fechas, categoría y precio máximo. El usuario los va aplicando de a uno en la interfaz, y cada filtro que agrega dispara una búsqueda nueva.
(d) TimezoneConverter: convierte fechas entre la zona del recinto y la del usuario. Usa la biblioteca estándar. Se llama desde ocho lugares.
Ver solución
(a) SeatMap — inyección, pero no del mapa: del cargador. Señales 1 y 2: no. Señal 3: sí, leer un archivo JSON cruza una frontera. Ahora el matiz que hace bueno el diagnóstico: lo que se inyecta no es el SeatMap —eso es un objeto de datos que se construye normal— sino quien lo carga. Separa las dos cosas: SeatMap es un dataclass puro y load_seat_map(venue_id) es la función que toca el disco. Así las pruebas de lógica de asientos construyen SeatMap a mano, sin archivos. Cuando un objeto de datos "se construye leyendo algo", casi siempre hay dos objetos ahí dentro: el dato y el cargador.
(b) Invoice — dataclass con __post_init__. Señal 1: no. Señal 2: los datos llegan juntos, así que no hay construcción por pasos. La regla de cuadre sí cruza campos, y eso es exactamente lo que __post_init__ resuelve. Señal 3: el folio probablemente venga de un servicio externo o de un contador en la base —eso sí se inyecta, o mejor, se calcula arriba y se pasa como dato—. Este caso está puesto para reforzar lo de la lección 4: una regla que cruza campos no basta para justificar un Builder.
(c) EventSearchQuery — Builder, y es el único de los cuatro. Señal 2, primer criterio: los filtros llegan en momentos distintos, a medida que el usuario los aplica. Y hay un beneficio adicional que este caso hace evidente: el objeto acumulado se puede serializar en la URL, que es como funcionan los buscadores reales. Aunque aquí conviene ser honesto: si cada with_x es solo una asignación, un dataclass inmutable con un método replace() —dataclasses.replace(query, city="CDMX")— hace lo mismo con menos código y devolviendo un objeto nuevo cada vez, que además evita bugs de estado compartido. Incluso donde el Builder aplica, Python suele tener algo más corto.
(d) TimezoneConverter — nada. Constructor o función suelta. Ninguna de las tres señales. Usa la biblioteca estándar, es determinista, no cruza ninguna frontera —las zonas horarias vienen con Python—. Ocho lugares que la llaman no es una señal de nada: es una función útil. Este caso está puesto porque "se usa en muchos lados" engaña: la cantidad de usos no es una señal; la cantidad de lugares que deciden, sí.
Por qué funciona: cuatro casos, un solo patrón del catálogo aplicado —y con reservas—. Si tu primer instinto fue meter un patrón en tres o cuatro, esa es la información valiosa del ejercicio, y es exactamente el síndrome del martillo nuevo que esta lección viene a tratar.
Ejercicio 2 — Desmonta la sobre-ingeniería. Este código apareció en un pull request de Boletia. Quítale todo lo que sobra sin perder ninguna funcionalidad, y escribe cuántas líneas y cuántos archivos ahorraste.
# Archivo: notifications/channel_factory.py
class NotificationChannelFactory:
def __init__(self):
self._registry = {}
self._register_defaults()
def _register_defaults(self):
self._registry["email"] = EmailChannelProvider()
def register(self, name, provider):
self._registry[name] = provider
def create(self, name):
provider = self._registry.get(name)
if provider is None:
raise ValueError(f"Canal no registrado: {name}")
return provider.provide()
# Archivo: notifications/channel_provider.py
class ChannelProvider(ABC):
@abstractmethod
def provide(self): ...
class EmailChannelProvider(ChannelProvider):
def provide(self):
return EmailChannelBuilder().with_host(settings.SMTP_HOST).build()
# Archivo: notifications/channel_builder.py
class EmailChannelBuilder:
def __init__(self):
self._host = None
def with_host(self, host):
self._host = host
return self
def build(self):
return EmailChannel(smtp_host=self._host)
# Uso, en el único lugar del sistema que manda notificaciones:
channel = NotificationChannelFactory().create("email")
channel.send(customer.email, message)
Ver solución
La versión equivalente:
# Archivo: app.py — en build_services()
notifier = EmailChannel(smtp_host=settings.SMTP_HOST)
# Uso:
notifier.send(customer.email, message)
Ahorro: tres archivos y unas 35 líneas, reemplazadas por una.
Vamos a contar lo que había y qué compraba cada pieza:
- Una Factory para un registro con una sola entrada. No decide nada:
create("email")siempre devuelve lo mismo. Es el error común número uno de la lección 2. - Un
register()que nadie llama nunca. Es una arquitectura de plugins en miniatura, para un solo plugin —el rincónplugins/del módulo 2 con otro traje—. - Una clase abstracta
ChannelProvidercon un solo implementador. Es el olor de la lección 6 del módulo 2. - Un
EmailChannelProvidercuyo único método construye otra cosa. Es una capa que solo reenvía. - Un Builder con un solo
with_x, que es una asignación. Es el anti-caso literal de la lección 4.
Cinco abstracciones apiladas para llamar a un constructor de un parámetro. Y —esto es lo importante— nada de esto se escribió por descuido. Cada pieza, por separado, imita un patrón legítimo. Quien lo escribió estaba aplicando lo que había aprendido, con buena fe. Ese es el punto de la lección: la sobre-ingeniería no viene de la ignorancia, viene del entusiasmo.
Y ahora la pregunta honesta: ¿qué se pierde al desmontarlo?
Nada de lo que existe hoy. Pero sí desaparece la capacidad de registrar canales desde afuera en tiempo de ejecución. ¿Boletia la necesita? No: los canales son tres, se conocen al compilar, y no hay ningún requerimiento de extensión por terceros. Si algún día Boletia vendiera integraciones donde un cliente registre su propio canal, esa capacidad haría falta —y ese día se construye, con la información de ese día—.
Lo que sí conviene dejar es una nota de una línea en el pull request donde lo quitas, explicando bajo qué condición volvería. Eso es lo que convierte un borrado en una decisión de diseño documentada, y es exactamente lo que el proyecto de la lección 8 te va a pedir escribir.
Ejercicio 3 — Escribe la condición de reversión. Para cada una de estas tres decisiones de "no abstraer" en Boletia, escribe la condición concreta y observable bajo la cual cambiarías de opinión. Tiene que ser algo que alguien pueda verificar sin discutir.
(a) No extraer una factory para los canales de notificación, porque solo notifier.py los construye.
(b) No inyectar el generador de códigos de barras, porque es una función pura.
(c) No usar un Builder para Invoice, porque todos los datos llegan juntos.
Ver solución
(a) "Extraemos la factory cuando un segundo archivo necesite construir un canal por su nombre. Hoy solo notifier.py lo hace; el candidato más probable es el panel de administración cuando agreguemos el botón de reenviar confirmación."
Es verificable —se cuentan los archivos— y además nombra al sospechoso, lo que ayuda a que alguien lo note cuando ocurra.
(b) "Lo inyectamos si el código de barras pasa a generarse con un servicio externo, o si el formato tiene que cambiar por recinto. Mientras sea una fórmula determinista dentro de nuestro código, se prueba llamándola directamente."
Nota que la condición está escrita sobre la propiedad que sostiene la decisión —ser puro y determinista— y no sobre un número. Cuando la razón es una propiedad, la condición de reversión es que esa propiedad deje de ser cierta.
(c) "Cambiamos a Builder si la factura pasa a emitirse por pasos: por ejemplo, si aparecen las facturas parciales que pidió administración, donde se agregan conceptos a lo largo del mes y se cierra al final."
Aquí la condición cita un requerimiento concreto que ya se mencionó en el equipo. Ese es el tipo de condición más útil: alguien puede reconocerla cuando llegue.
Por qué este ejercicio es el más importante de la lección. Una decisión de no abstraer, escrita sin su condición de reversión, es indistinguible de no haber pensado. Seis meses después, cuando alguien mire el if que creció, no va a poder saber si fue una decisión o un descuido —y va a asumir lo segundo, porque es lo más frecuente—.
Con la condición escrita, esa misma decisión se vuelve dos cosas valiosas a la vez: evidencia de que hubo criterio y un disparador para el futuro. Alguien lee "cuando un segundo archivo construya un canal" y sabe qué hacer.
Un lugar práctico donde ponerlas: un comentario corto donde vive el código, o —mejor— la descripción del pull request. Un comentario de tres líneas que diga "decisión: no extraemos factory porque solo este archivo decide. Revisar cuando haya un segundo." vale más que cualquier diagrama.
Y este es exactamente el entregable que te va a pedir la lección 8. El proyecto no se juzga por cuántos patrones aplicaste: se juzga por lo que decidiste no hacer y por qué.
Resumen y siguiente paso
En esta lección pusiste el filtro delante de todo lo que aprendiste en el módulo. La idea de fondo: la mayoría de los objetos de un sistema se construyen con un constructor y ya, y los patrones de creación viven en el borde del sistema —donde hay decisiones repetidas, donde se habla con lo externo— y no en su núcleo. El corazón del dominio de Boletia —Ticket, Order, Money, Customer— no necesita nada.
Tienes las tres señales de que un patrón sí se gana su lugar, cada una con su prueba concreta: la decisión se repite —¿cuántos archivos abro para agregar una opción?—, la construcción es compleja de verdad —¿mis pasos hacen algo o solo guardan?— y necesitas sustituir la pieza —¿alguna vez le voy a pasar algo distinto?—. Y las tres falsas que engañan a todo el mundo: "hay un if", "algún día vamos a necesitar otro" y "así se ve más profesional".
Aplicaste el filtro a cinco rincones de Boletia y el resultado fue el que importa: ninguna Factory nueva, ningún Builder, dos casos de inyección, uno de conocimiento que estaba en el archivo equivocado, y dos que se quedaron intactos. Esa proporción es la del trabajo real, y trae la lección lateral más útil del módulo: antes de preguntarte qué patrón aplicar, pregúntate si algo está simplemente en el lugar equivocado. Mover conocimiento a donde pertenece no tiene nombre de catálogo y resuelve más problemas que cualquier patrón.
Y tienes dos formas de medir la ceremonia para que tu opinión en una revisión no suene a gusto personal: contar los saltos de lectura que exige responder una pregunta concreta, y la proporción entre líneas de ceremonia y líneas de trabajo.
Antes de avanzar deberías poder: aplicar las tres señales a un caso que nunca viste; desmontar una pila de abstracciones sin perder funcionalidad; explicar por qué "simple" no es lo mismo que "corto"; y —lo que más va a pesar en el proyecto— escribir la condición de reversión de una decisión de no abstraer, de forma que alguien pueda verificarla sin discutir.
La lección 8 es el proyecto, y ahora ya sabes por qué está planteado como está. Vas a tomar la creación dispersa de PaymentProvider y NotificationChannel en Boletia y a ordenarla con el patrón mínimo que resuelva el problema, entregando además un documento corto con lo que decidiste no abstraer y bajo qué condición lo reconsiderarías. Se juzga por la simplicidad de la solución, no por su sofisticación —y después de esta lección, esa frase ya no debería sonar a consuelo.
Recursos
- The Grug Brained Developer — el ensayo sobre la complejidad como el enemigo real, contado con humor. Su sección sobre el "factory factory" es esta lección en tres párrafos.
- Sandi Metz — The Wrong Abstraction — el ensayo que popularizó la frase "duplicación es más barata que la abstracción equivocada". Es el argumento central de la señal falsa número dos.
- Martin Fowler — Yagni — la formulación precisa del principio, con el matiz que casi siempre se pierde: no es "nunca pienses en el futuro", es "no construyas para el futuro que no está pedido".
- Dan North — Introducing Deliberate Discovery — por qué la información que más te falta es justamente la que necesitarías para diseñar bien, y qué hacer con eso. Es el fundamento teórico de "espera al tercer caso".