Módulo 3: Patrones para el comportamiento que varía
8. Proyecto: reemplaza un `if` gigante con criterio
Descripción
Al terminar este proyecto vas a haber hecho el trabajo completo que este módulo prepara: tomar el checkout de Boletia tal como está —con sus condicionales de todos los tipos—, decidir cuáles merecen extraerse y cuáles no, aplicar a los que sí el patrón y el peso adecuados, y entregar el código junto con un documento que justifica cada decisión, incluidas las de no tocar nada.
Léelo dos veces: incluidas las de no tocar nada. Este proyecto no se evalúa por cuántos patrones aplicaste. Se evalúa por el criterio con el que decidiste y por lo bien que puedes explicarlo. Un trabajo con dos extracciones bien argumentadas y cinco "no" bien argumentados vale más que uno con seis patrones y ninguna justificación. Es la diferencia entre alguien que aplica patrones y alguien que decide sobre ellos, y es exactamente la habilidad que se defiende en una entrevista.
Vas a producir tres cosas: el inventario de condicionales con un veredicto y su evidencia por cada uno; el código refactorizado en pasos pequeños, con el comportamiento intacto y probado; y el documento DECISIONES.md, que es donde de verdad se ve si entendiste el módulo.
Conexión con el módulo: aquí se junta todo. La lección 1 te dio el eje de variación y las tres señales. Las lecciones 2 y 3 te dieron Strategy, su anatomía y el refactor en pasos pequeños con red de seguridad. La 4 trajo Template Method para el orden fijo con pasos variables, la 5 State para el comportamiento que depende del ciclo de vida, la 6 el criterio para elegir el peso del mecanismo —función, diccionario, clausura, clase— y la 7 el filtro para decidir qué merece salir. El proyecto te pide usar las siete a la vez sobre un código que no escribiste, que es como llega el trabajo real.
Antes de la mudanza: el inventario
Cuando alguien se muda de casa, hay dos formas de hacerlo. La primera es empezar a empacar por donde uno esté parado: la cocina, porque es lo que tienes enfrente. Al tercer día descubres que empacaste la vajilla de diario y que llevas cuatro cajas de cosas que ibas a tirar.
La segunda —la que hace la gente que ya se mudó varias veces— es recorrer la casa entera antes de empacar nada, con una libreta, y clasificar: esto va, esto se dona, esto se tira, esto se queda porque es del departamento. El recorrido cuesta una tarde. Y ahorra una semana, porque después cada decisión ya está tomada y solo hay que ejecutarla.
Fíjate en una cosa de ese inventario: la mayoría de las anotaciones dicen "se queda" o "se tira", no "se empaca". Ese es exactamente el resultado que debe tener el tuyo. Recorrer el código y anotar veredictos no es la parte previa al trabajo: es el trabajo. Lo que viene después —extraer, mover, probar— es ejecución mecánica de decisiones que ya tomaste.
Y hay algo más que el inventario te da y que la gente subestima: queda escrito por qué no hiciste algo. Dentro de seis meses, cuando alguien mire el checkout y pregunte "¿por qué esto sigue siendo un if?", la respuesta va a estar en DECISIONES.md en vez de en la memoria de alguien que ya no está en el equipo. Un "no" documentado vale tanto como un refactor hecho, porque evita que alguien lo deshaga por accidente.
Ejemplo trabajado: una decisión completa, de principio a fin
Antes de darte el código, vamos a hacer una decisión juntos, entera, para que tengas el molde. Voy a tomar uno de los condicionales del checkout y a llevarlo desde el inventario hasta la justificación escrita.
El condicional:
# Aparece en checkout/checkout.py
if order.provider == "stripe":
client = StripeClient(api_key=settings.STRIPE_KEY)
result = client.create_charge(amount=int(order.total * 100), currency="MXN")
external_id = result["id"]
elif order.provider == "mercadopago":
client = MercadoPagoClient(token=settings.MP_TOKEN)
result = client.pay(order.total, description=f"Boletia #{order.id}")
external_id = str(result["payment_id"])
elif order.provider == "cash":
external_id = None
order.cash_reference = generate_cash_reference(order.id)
else:
raise ValueError(f"Proveedor desconocido: {order.provider}")
Paso 1 — Medir las tres señales. No opinar: medir.
¿Crece? Al historial:
git log --oneline -- checkout/checkout.py | head -20
Encuentras tres commits que dicen "agrega MercadoPago", "agrega pago en efectivo" y "soporte para OXXO Pay (revertido)". Tres cambios en dieciocho meses, todos agregando una rama al mismo condicional. Señal 1: sí.
¿Se repite? A buscar:
grep -rn "order.provider ==" --include=*.py .
Aparece en checkout/checkout.py, en reports/sales_report.py —donde calcula la comisión— y en api/routes.py —donde decide a qué página redirigir—. Tres archivos. Señal 2: sí.
¿Mezcla decidir con hacer? Cada rama construye un cliente, llama a una API externa, traduce la respuesta y puede fallar de formas distintas. Entre cuatro y seis líneas con efectos. Señal 3: sí.
Tres de tres. El umbral era dos.
Paso 2 — Elegir el patrón y el peso. Es una Strategy: varias formas de hacer lo mismo, elegidas según un dato de la orden.
¿Con funciones o con clases? Aplica las cuatro razones de la lección 6. Hay estado —el cliente HTTP con su llave, que conviene construir una vez— y hay varias operaciones relacionadas, porque los otros dos archivos piden commission_for(order) y checkout_url(order) sobre el mismo concepto. Dos de las cuatro razones. Clases.
Y fíjate en lo que decidiste de paso: el contrato no es solo charge. Si lo defines mirando únicamente el checkout, dentro de un mes tienes que ampliarlo. El inventario te lo dijo antes.
Paso 3 — Los pasos del refactor. Como en la lección 3, y en este orden:
- Pruebas de caracterización del
chargeactual, con las tres rutas y el error del proveedor desconocido. - Extraer cada rama a una función, sin cambiar la firma del llamador.
- Escribir el contrato leyendo las tres firmas y agregando las dos operaciones que piden los otros archivos.
- Convertir en clases, con la configuración por constructor.
- Mover la elección a un solo lugar.
- Reemplazar los condicionales de
reports/yapi/por llamadas al mismo objeto.
El paso 6 es el que devuelve más y el que más se olvida. Si solo arreglas el checkout, el condicional sigue vivo en dos archivos y no eliminaste la señal 2 —la más cara de las tres—.
Paso 4 — La justificación escrita. Así se ve una entrada de DECISIONES.md:
C-03 ·
order.providerencheckout.py,sales_report.pyyroutes.pyVeredicto: extraer. Strategy con clases (
PaymentProvider).Evidencia. Crece: tres commits en 18 meses agregaron una rama (MercadoPago, efectivo, OXXO revertido). Se repite: el campo se compara en tres archivos, y el de
routes.pyya se desfasó —le falta una rama—. Mezcla decidir con hacer: cada rama construye un cliente, llama a una API externa y traduce la respuesta.Peso elegido y por qué. Clases y no funciones, por dos de las cuatro razones: hay estado (el cliente con su llave, que se construye una vez) y hay tres operaciones relacionadas sobre el mismo concepto (
charge,commission_for,checkout_url).Qué cuesta. Cinco archivos donde había un bloque, y dos saltos de lectura para saber cómo cobra Stripe. Aparece un tipo nuevo,
ChargeResult, que hay que mantener.Cuándo no lo habría hecho. Con un solo proveedor real, o si los tres se diferenciaran solo en una tasa —eso sería una tabla—. Tampoco si el condicional viviera únicamente en
checkout.pyy no se hubiera dispersado: con una sola copia, la urgencia baja bastante.
Cinco bloques cortos. Evidencia medida, no opinión. El peso justificado con las razones de la lección 6. El costo dicho de frente. Y la condición de "no" al final, que es la que demuestra que hubo decisión.
Qué esperar de tu propio inventario. Vas a encontrar entre ocho y doce condicionales relevantes. Si tu inventario termina con más de tres o cuatro extracciones, revísalo: probablemente marcaste guardas o validaciones. Y si termina con cero, revísalo también. La forma sana de un inventario de este código se parece a: dos o tres extracciones, una o dos mejoras baratas de nombre o de tabla, y el resto sin tocar.
El código: el checkout completo de Boletia
Aquí está. Es la función que orquesta una compra, con todo lo que se le fue pegando en tres años. No está escrita por alguien descuidado: está escrita por cinco personas distintas, en momentos distintos, cada una resolviendo lo suyo con prisa.
# Archivo: checkout/checkout.py
# Boletia — el corazón del sistema. Léelo entero antes de tocar nada.
MAX_TICKETS_PER_ORDER = 20
SERVICE_FEE_RATE = 0.08
COURTESY_LIMIT_PER_EVENT = 50
def checkout(order, customer, now):
"""Procesa una compra completa: valida, cobra, emite y notifica."""
# ---------- 1. Validaciones de entrada ----------
if not order.ticket_ids:
raise InvalidOrder("La orden no tiene boletos")
if len(order.ticket_ids) > MAX_TICKETS_PER_ORDER:
raise InvalidOrder(f"Máximo {MAX_TICKETS_PER_ORDER} boletos por orden")
if order.status != "pending":
raise InvalidOperation(f"No se puede procesar una orden {order.status}")
# ---------- 2. Precio: un bloque por tipo de boleto ----------
total = 0.0
for ticket in load_tickets(order.ticket_ids):
if ticket.kind == "general":
price = ticket.base_price
if len(order.ticket_ids) >= 10:
price = price * 0.95
price = price + price * SERVICE_FEE_RATE
elif ticket.kind == "vip":
price = ticket.base_price * 1.35 + 150.0
if customer.is_member:
price = price * 0.90
price = price + price * SERVICE_FEE_RATE
elif ticket.kind == "early_bird":
event = get_event(ticket.event_id)
cutoff = parse_date(event.early_bird_cutoff)
if now <= cutoff:
price = ticket.base_price * 0.70
else:
price = ticket.base_price
price = price + price * SERVICE_FEE_RATE
elif ticket.kind == "courtesy":
issued = count_courtesies(ticket.event_id)
if issued >= COURTESY_LIMIT_PER_EVENT:
raise TooManyCourtesies("Se agotaron las cortesías del evento")
price = 0.0
else:
raise ValueError(f"Tipo de boleto desconocido: {ticket.kind}")
total += round(price, 2)
order.total = round(total, 2)
# ---------- 3. Cobro ----------
if order.total == 0:
# Órdenes de puras cortesías: no hay nada que cobrar.
order.status = "paid"
order.paid_at = now
external_id = None
else:
if settings.PAYMENT_SANDBOX:
# Ambientes de prueba: no cobramos de verdad.
# Se quita al cerrar la integración de MercadoPago (BOL-1180).
external_id = "sandbox-approved"
order.status = "paid"
elif order.provider == "stripe":
client = StripeClient(api_key=settings.STRIPE_KEY)
result = client.create_charge(amount=int(order.total * 100),
currency="MXN")
external_id = result["id"]
order.status = "paid"
order.paid_at = now
elif order.provider == "mercadopago":
client = MercadoPagoClient(token=settings.MP_TOKEN)
result = client.pay(order.total, description=f"Boletia #{order.id}")
external_id = str(result["payment_id"])
order.status = "paid"
order.paid_at = now
elif order.provider == "cash":
# El efectivo no cobra ahora: emite una referencia y espera.
external_id = None
order.cash_reference = generate_cash_reference(order.id)
# OJO: la orden se queda en "pending" a propósito.
else:
raise ValueError(f"Proveedor desconocido: {order.provider}")
order.external_payment_id = external_id
# ---------- 4. Asignación de asientos ----------
event = get_event(order.event_id)
if event.has_numbered_seats:
plugin = PluginRegistry.get("seating") # el rincón de plugins
plugin.assign(order)
# ---------- 5. Puntos de lealtad ----------
if customer.is_member and order.total > 5000:
add_loyalty_points(customer, int(order.total / 100))
# ---------- 6. Notificaciones ----------
message = _confirmation_text(order)
if customer.email is not None:
SmtpClient(host=settings.SMTP_HOST).send(
to=customer.email, subject="Tu compra en Boletia", body=message)
if customer.phone is not None:
SmsGateway(key=settings.SMS_KEY).send_text(
number=customer.phone, text=message[:160])
if customer.push_token is not None:
PushService(cert=settings.PUSH_CERT).notify(
token=customer.push_token, payload={"body": message})
if order.provider == "cash":
# Solo el efectivo necesita instrucciones de pago.
send_payment_instructions(customer, order)
organizer = get_customer(event.organizer_id)
if organizer.email is not None:
SmtpClient(host=settings.SMTP_HOST).send(
to=organizer.email, subject=f"Nueva venta: {event.name}",
body=f"Se vendieron {len(order.ticket_ids)} boletos")
return order
def _confirmation_text(order):
if order.status == "pending":
return "Tu orden está pendiente de pago"
elif order.status == "paid":
return "¡Listo! Tus boletos están confirmados"
elif order.status == "cancelled":
return "Tu orden fue cancelada"
return "Estado desconocido"
Y dos fragmentos de otros archivos que te van a hacer falta para medir la señal 2:
# Archivo: reports/sales_report.py
if order.provider == "stripe":
fee = order.total * 0.036 + 3.0
elif order.provider == "mercadopago":
fee = order.total * 0.0399
elif order.provider == "cash":
fee = 12.0
# Archivo: api/routes.py
if order.provider == "stripe":
return redirect(stripe_checkout_url(order))
elif order.provider == "mercadopago":
return redirect(mp_checkout_url(order))
# (el efectivo no está: se agregó después y nadie actualizó esto)
Ese comentario de la última línea no es un adorno. Es la señal 2 con las manos en la masa: una copia del condicional que se quedó atrás, y el bug que produce —una redirección rota para las órdenes en efectivo— lleva ahí un tiempo indeterminado.
Los entregables y cómo se evalúa
Entregable 1 — INVENTARIO.md
Una tabla con todos los condicionales del código de arriba. Uno por fila, sin saltarte ninguno, aunque el veredicto sea "no tocar". La tabla tiene esta forma:
| ID | Dónde | Condición | ¿Crece? | ¿Se repite? | ¿Mezcla? | Veredicto |
|---|---|---|---|---|---|---|
| C-01 | checkout.py §1 | not order.ticket_ids | No | No | No | Dejar |
| C-02 | checkout.py §2 | ticket.kind == … | Sí | Sí | Sí | Extraer |
| … |
Los veredictos permitidos son cuatro, y conviene que uses los cuatro: Dejar, Dejar + mejorar (con el escalón de la lección 7: nombre, guarda, tabla o tipo), Extraer, y Borrar (para lo que es temporal y ya cumplió).
Entregable 2 — el código refactorizado
Con tres condiciones no negociables:
El comportamiento no cambia. Escribe primero las pruebas de caracterización del checkout actual, y que sigan en verde al final. Si encuentras un bug —hay al menos uno en el código de arriba— no lo arregles en el mismo commit: anótalo en DECISIONES.md, arréglalo aparte y con su propia prueba.
Pasos pequeños. Un commit por movimiento, con las pruebas corriendo entre uno y otro. El historial es parte de la entrega: si tu refactor es un solo commit de trescientas líneas, no se puede revisar.
El peso mínimo que resuelva el problema. Aplica la escalera de la lección 6. Si un diccionario de funciones basta, no escribas cinco clases.
Entregable 3 — DECISIONES.md
Una entrada por cada condicional que marcaste como Extraer, Dejar + mejorar o Borrar, y por cada Dejar cuya decisión no sea obvia. Con los cinco bloques del ejemplo trabajado: veredicto, evidencia medida, peso elegido y por qué, qué cuesta, y cuándo no lo habría hecho.
Al final, una sección corta de "lo que no toqué y por qué", agrupando los "dejar" evidentes. Tres o cuatro líneas bastan: "Las guardas de customer.email, customer.phone y customer.push_token se quedan: son comprobaciones binarias que no pueden crecer. La disponibilidad por canal sí se va a modelar cuando toquemos notificaciones, pero eso es el módulo 6 y no lo adelanto aquí."
Cómo se evalúa
| Peso | Qué se mira |
|---|---|
| ★★★ | Los "no" justificados. Que las decisiones de no extraer tengan evidencia y no sean omisiones. Es lo que más pesa |
| ★★★ | La evidencia sobre la opinión. Historial, búsquedas, conteos. "Me parece feo" no cuenta |
| ★★ | El peso del mecanismo. Que no haya clases donde bastaba una función, ni funciones sueltas donde hacía falta un objeto |
| ★★ | El comportamiento intacto, demostrado con pruebas escritas antes de tocar |
| ★★ | Los pasos pequeños, visibles en el historial |
| ★ | Que el inventario esté completo, sin condicionales olvidados |
| ★ | Que se haya cerrado la señal 2: si extraes el proveedor, que también se arreglen reports/ y api/ |
Fíjate en lo que no está en la tabla: la cantidad de patrones aplicados. No suma. Y si aplicas uno sin justificarlo, resta.
Autoevaluación antes de entregar
- ¿Mi inventario tiene más "dejar" que "extraer"? (Si no, revísalo.)
- ¿Cada veredicto tiene una razón que no es "hay un
if"? - ¿Puedo señalar el comando o la búsqueda con que medí cada señal?
- ¿Escribí las pruebas antes de tocar el código, y siguen en verde?
- ¿Alguna de mis extracciones podría hacerse con la mitad de peso —una función en vez de una clase, un diccionario en vez de una jerarquía—?
- ¿Cada entrada de
DECISIONES.mdtiene su "cuándo no lo habría hecho"? - ¿El bug que encontré está anotado y arreglado en un commit aparte?
- Si alguien discrepara con una de mis decisiones, ¿tengo con qué conversar sin que sea cuestión de gustos?
Errores comunes
Empezar a refactorizar antes de terminar el inventario (de proceso). Qué pasa: alguien lee el código, ve el bloque de precios —que es el más llamativo—, y se pone a extraerlo. Tres horas después tiene pricing/ armado, y recién entonces descubre que el condicional del proveedor está repartido en tres archivos y que su solución de precios no previó que el cargo por servicio no aplica a cortesías. Rehace medio trabajo. Por qué pasa: extraer se siente productivo desde el primer minuto e inventariar se siente como no avanzar. Cómo detectarlo: si abriste el editor antes que la libreta, ya estás ahí. Cómo corregirlo: el inventario completo primero, sin excepción, aunque sean cuarenta minutos que se sienten perdidos. Es la tarde de recorrer la casa que ahorra la semana de empacar mal. Y trae un beneficio extra: mientras inventarías encuentras las dependencias entre condicionales —que el bloque de precios y el de cobro comparten order.total— y eso decide el orden en que conviene tocarlos.
Extraer el bloque más grande porque es el más grande (de criterio). Qué pasa: el bloque de precios ocupa cuarenta líneas y salta a la vista, así que se extrae primero y a veces se extrae solo. Mientras tanto, el condicional del proveedor —que es más chico pero está repartido en tres archivos y ya tiene una copia desfasada con un bug— se queda como está. Por qué pasa: el tamaño es visible y la dispersión no; hay que buscarla. Cómo detectarlo: si tu orden de trabajo coincide con el orden de arriba abajo del archivo, no priorizaste, leíste. Cómo corregirlo: ordena por daño, no por tamaño. Un condicional repartido en varios archivos hace más daño que uno grande pero solo, porque el grande falla de forma visible cuando se rompe y el repartido falla en silencio en la copia que nadie actualizó.
Entregar solo el código (de comunicación). Qué pasa: el refactor está bien hecho, las pruebas pasan, y la entrega es un PR con quinientas líneas y el título "refactor del checkout". Quien revisa no puede evaluar el criterio, porque el criterio no está escrito en ningún lado —está en la cabeza de quien lo hizo—. La revisión se vuelve una discusión de estilo, y las decisiones de no extraer son invisibles: no se ve la diferencia entre "lo pensé y decidí que no" y "no lo vi". Por qué pasa: el código se siente como el entregable y el documento como burocracia. Cómo detectarlo: si tu entrega no permite distinguir un "no" deliberado de un olvido, falta el documento. Cómo corregirlo: DECISIONES.md no es un adorno académico; en un equipo real es lo que hace que dentro de un año nadie deshaga tu decisión por accidente. Y en una entrevista, es literalmente lo que se te pide: "cuéntame un refactor que hiciste y por qué tomaste esas decisiones".
Ejercicios
Ejercicio 1 — Haz el inventario completo. Recorre el checkout de arriba y llena la tabla de INVENTARIO.md con todos los condicionales. Para cada uno, marca las tres señales y emite veredicto. No refactorices nada todavía.
Ver solución
Un inventario razonable. El tuyo puede diferir en algún veredicto —lo importante es la razón, no la coincidencia—.
| ID | Condición | Crece | Repite | Mezcla | Veredicto |
|---|---|---|---|---|---|
| C-01 | not order.ticket_ids | No | No | No | Dejar (validación) |
| C-02 | len(...) > MAX_TICKETS | No | No | No | Dejar (validación) |
| C-03 | order.status != "pending" | No | Sí | No | Dejar + mejorar (ver nota) |
| C-04 | ticket.kind == … (4 ramas) | Sí | Sí | Sí | Extraer |
| C-05 | len(order.ticket_ids) >= 10 | No | No | No | Dejar (interna de general) |
| C-06 | customer.is_member (en VIP) | No | No | No | Dejar (interna de VIP) |
| C-07 | now <= cutoff | No | No | No | Dejar (es la regla early-bird) |
| C-08 | issued >= COURTESY_LIMIT | No | No | No | Dejar (validación de la regla) |
| C-09 | order.total == 0 | No | No | No | Dejar + nombrar |
| C-10 | settings.PAYMENT_SANDBOX | No | No | No | Borrar al cerrar BOL-1180 |
| C-11 | order.provider == … (×3 archivos) | Sí | Sí | Sí | Extraer |
| C-12 | event.has_numbered_seats | No | No | No | Dejar (ver nota) |
| C-13 | is_member and total > 5000 | No | No | No | Dejar + nombrar |
| C-14 | customer.email is not None | No | Sí | No | Dejar (ver nota) |
| C-15 | customer.phone is not None | No | Sí | No | Dejar |
| C-16 | customer.push_token is not None | No | Sí | No | Dejar |
| C-17 | order.provider == "cash" (notificación) | No | Sí | No | Se resuelve con C-11 |
| C-18 | organizer.email is not None | No | Sí | No | Dejar |
| C-19 | order.status == … en _confirmation_text | Sí | Sí | No | Dejar + tabla |
Diecinueve condicionales, dos extracciones. Esa proporción es la buena noticia del ejercicio.
Cuatro notas que valen más que la tabla:
C-03 y C-19 son territorio de State. Los dos consultan order.status, y en la lección 5 viste que ese campo se compara en varios métodos de Order. La decisión correcta aquí es no resolverlo dentro del checkout: el ciclo de vida de la orden es responsabilidad de Order, no del orquestador. Lo que sí conviene es que checkout pregunte if not order.can_be_processed: y que _confirmation_text sea order.confirmation_message. Eso mueve la decisión a donde vive el estado sin montar clases todavía. Y _confirmation_text es un mapeo de estado a texto: escalón 3, un diccionario.
C-12 es una trampa deliberada. Parece un candidato porque toca el PluginRegistry. No lo extraigas: ese rincón es el del módulo 2 —la arquitectura de plugins para una sola implementación— y lo que hay que hacerle es quitarle estructura, no agregarle. Aquí, en este proyecto, se deja tal cual y se anota. Reconocer que un problema no es el tuyo también es criterio.
C-14 a C-18 son guardas, pero hay algo más. Individualmente cada una es una comprobación binaria que no crece. Juntas, sin embargo, forman un patrón: "para cada canal, si el cliente lo tiene disponible, manda". Eso apunta a NotificationChannel y a la pregunta de a quién hay que avisar, que es el módulo 6. En este proyecto se dejan, y se anota por qué: la decisión de no adelantarse a un módulo posterior es legítima y hay que escribirla.
El bug. En C-11, la rama de efectivo deja order.status en "pending" a propósito —está comentado—, pero api/routes.py no tiene esa rama y redirige mal. Anótalo, no lo arregles junto con el refactor.
Por qué funciona: el inventario te obliga a mirar los diecinueve antes de enamorarte de dos. Y te hace descubrir que tres de ellos pertenecen a otros módulos, que es información de diseño que no aparece si empiezas a extraer.
Ejercicio 2 — Refactoriza y mide. Ejecuta las dos extracciones y las mejoras baratas. Al terminar, responde con números: (a) ¿cuántos archivos hay ahora?, (b) ¿cuántas líneas tiene checkout()?, (c) ¿qué habría que tocar para agregar un cuarto proveedor?, (d) ¿qué habría que tocar para agregar un quinto tipo de boleto?
Ver solución
Los números dependen de tus decisiones, pero el orden de magnitud debería parecerse a esto.
(a) Archivos. De 1 a unos 8: pricing/rules.py, pricing/context.py, pricing/calculator.py, payments/provider.py y tres proveedores, más checkout.py. Si te salieron quince, revisa el peso: probablemente hay clases donde bastaban funciones, o un archivo por regla donde cabían las cuatro en uno.
(b) Líneas de checkout(). De unas 120 a unas 40. Y lo que importa no es el número sino qué quedó dentro: solo la orquestación —valida, calcula, cobra, asigna, notifica— sin una sola decisión de negocio. Si al leer las cuarenta líneas se entiende el flujo completo de una compra sin abrir nada más, el refactor cumplió.
(c) Cuarto proveedor: un archivo nuevo, una línea en el registro. checkout.py no se abre, sales_report.py no se abre, routes.py no se abre —porque las tres operaciones viven en el mismo objeto—. Si tu respuesta incluye tocar reports/ o api/, no cerraste la señal 2 y el trabajo está a medias.
(d) Quinto tipo de boleto: una regla nueva, una línea en el registro, y quizá un campo con valor por defecto en el contexto. checkout.py no se abre.
Y una respuesta más que conviene que te des aunque nadie la pida: ¿qué NO mejoró? Leer el checkout por primera vez ahora exige abrir tres o cuatro archivos más. Alguien que entre al equipo tarda más en entender cómo se cobra un VIP. Ese costo es real y lo pagas a cambio de los cuatro números anteriores. Escríbelo en DECISIONES.md: un refactor que solo enumera ganancias no fue evaluado, fue vendido.
Por qué funciona: cerrar con números convierte una sensación en un argumento. "Quedó más limpio" no se puede discutir ni defender; "agregar un proveedor pasó de tocar tres archivos a tocar uno" sí.
Ejercicio 3 — Defiende una decisión que te van a discutir. Elige la decisión más discutible de tu trabajo —normalmente es un "dejar" que alguien va a querer extraer, o una extracción que alguien va a considerar excesiva— y escribe el intercambio completo: el comentario que te harían, tu respuesta, y qué evidencia pedirías o traerías para cerrar la conversación. Máximo doce líneas.
Ver solución
No hay una respuesta única. Un intercambio bien construido se parece a esto:
Comentario que me harían: "¿Por qué dejaste los tres
ifde notificación? Eso pide unNotificationChannelconis_available_forysend, tal cual lo vimos. Se ve igual que el caso del proveedor."Mi respuesta: "Estoy de acuerdo en que va a terminar ahí, y probablemente pronto. Lo dejé fuera de este cambio por dos razones. La primera es de alcance: el PR ya toca precios y proveedores, y meter notificaciones lo vuelve imposible de revisar con cuidado —justo en el archivo donde menos conviene equivocarse—. La segunda es que el problema real de las notificaciones no es solo el canal: es a quién hay que avisarle. Hoy son el cliente y el organizador, y ya se habló de agregar a facturación. Si extraigo ahora solo el canal, en un mes voy a tener que rehacerlo para meter el otro eje. Prefiero esperar a tener los dos y diseñar una vez. Lo dejé anotado en
DECISIONES.mdcon esa condición: cuando entre el tercer destinatario, se abre."Evidencia que traería: el tamaño del PR actual, y la referencia al pedido de facturación —un ticket, un mensaje, lo que exista—. Si no existe ese pedido, mi segundo argumento se debilita y conviene decirlo.
Fíjate en cuatro cosas.
No niega que el otro tenga razón. Empieza reconociendo que sí va a terminar ahí. Discutir el destino cuando estás de acuerdo con el destino es perder la conversación por el lado equivocado.
Distingue "no" de "todavía no". Casi todas las decisiones de este proyecto son "todavía no", y decirlo así cambia el tono por completo.
Da la condición de apertura. "Cuando entre el tercer destinatario" es verificable. Un "más adelante" no lo es.
Admite dónde el argumento es débil. La última línea —"si no existe ese pedido, mi segundo argumento se debilita"— es lo que separa una defensa de una justificación. Y es lo que hace que la próxima vez te crean.
Por qué funciona: en el trabajo real, la mitad del valor de una buena decisión se pierde si no la puedes sostener en una conversación. Y en una entrevista, esto es la pregunta: no te van a pedir que escribas una Strategy en el pizarrón, te van a pedir que cuentes un cambio que hiciste y por qué. Este ejercicio es el ensayo de esa respuesta.
Resumen y siguiente paso
Cerraste el módulo haciendo el trabajo completo sobre el checkout de Boletia: inventariar, juzgar, extraer con criterio y justificar cada decisión, incluidas las de no tocar nada.
Empezaste por el inventario, que no es la parte previa al trabajo sino el trabajo. Recorriste diecinueve condicionales y encontraste que solo dos merecían extracción —el bloque de precios y el del proveedor de pago, este último por estar repartido en tres archivos con una copia ya desfasada—. Varios se quedaron con una mejora de una línea: un nombre, una tabla. Uno había que borrarlo en vez de abstraerlo. Y tres pertenecían a otros módulos, que también es un veredicto legítimo.
Tienes el molde de una decisión completa: medir las tres señales con comandos concretos en vez de intuirlas, elegir el patrón y su peso con las cuatro razones de la lección 6, refactorizar en pasos pequeños con red, y escribir la entrada de DECISIONES.md con sus cinco bloques —veredicto, evidencia, peso y por qué, qué cuesta, y cuándo no lo habría hecho—.
Y te llevas la forma de cerrar cualquier refactor: con números, no con adjetivos. Cuántos archivos, cuántas líneas quedaron en el orquestador, qué se toca para agregar el caso siguiente, y qué empeoró, porque un refactor que solo enumera ganancias no fue evaluado.
Antes de avanzar deberías poder: producir un inventario completo con veredictos y evidencia; explicar el peso que elegiste para cada extracción; y sostener en una conversación una decisión de no extraer, con su condición de apertura.
Hay una pregunta que este módulo dejó abierta a propósito, y la vas a haber sentido al escribir el registro de proveedores. Cuando pusiste RULES = {"general": GeneralPricing(), ...} o cuando escribiste build_provider(name), decidiste cómo se construyen esos objetos: dónde viven sus llaves de API, quién los instancia, si se crean una vez al arrancar o en cada llamada, y qué pasa cuando uno necesita otro para funcionar. Ese es un problema distinto del de este módulo —aquí resolvimos qué hace cada pieza, no quién la fabrica— y tiene su propia familia de patrones.
El módulo 4 la trata: Factory para decidir qué crear en un solo lugar, Builder para armar algo complicado por partes, Singleton —del que más hay que desconfiar— e inyección de dependencias en palabras simples. Con la misma pregunta de fondo, que a estas alturas ya es tuya: cuándo un constructor común y corriente alcanza.
Recursos
- Michael Feathers — Working Effectively with Legacy Code — el manual de operar código que ya funciona: pruebas de caracterización, costuras, pasos seguros. Es el libro de este proyecto.
- Martin Fowler — Refactoring, catálogo en línea — las fichas de los movimientos que vas a usar:
Decompose Conditional,Replace Conditional with Polymorphism,Introduce Parameter Object,Replace Type Code with State/Strategy. - Sandi Metz — The Wrong Abstraction — por qué equivocarse hacia el lado de abstraer sale más caro. Léelo antes de decidir tus veredictos, no después.
- Martin Fowler — Yagni — el respaldo escrito de casi todas tus decisiones de "todavía no". Útil para citar en una revisión cuando la conversación se pone sobre gustos.