Mejorar código con code smells no significa reescribir todo. La forma más segura suele ser avanzar con cambios pequeños, ejecutar pruebas con frecuencia y separar claramente cambios de comportamiento de cambios de estructura.
En este tema veremos un flujo práctico para mejorar código existente usando pruebas como red de seguridad.
Una mejora segura es un cambio que reduce complejidad, duplicación o confusión sin alterar el comportamiento esperado. Para lograrlo, necesitamos saber cuál es el comportamiento actual y comprobarlo después de cada modificación.
Ejemplos de mejoras seguras:
Antes de tocar código, ejecuta las pruebas:
python -m pytest
Si las pruebas fallan antes de modificar, no tienes una base confiable. Primero debes entender si el fallo es conocido, si el entorno está mal o si el proyecto ya estaba roto.
Cuando el código no tiene pruebas, agrega pruebas de caracterización para capturar el comportamiento actual.
def test_calcular_total_actual():
productos = [
{"precio": 3000, "cantidad": 2},
{"precio": 1500, "cantidad": 1},
]
assert calcular_total_venta(productos, "vip", "AR") == 9213.75
Estas pruebas no necesariamente afirman que el diseño sea bueno. Sirven para evitar cambios accidentales.
Evita mezclar muchos cambios en una sola edición. Por ejemplo, no conviene renombrar variables, extraer funciones, cambiar reglas y modificar formato al mismo tiempo.
Un flujo más seguro:
Partimos de una función con varios smells:
def proc(items, c, p):
t = 0
for x in items:
t += x["precio"] * x["cantidad"]
if c == "vip":
t *= 0.85
if p == "AR":
t *= 1.21
if t < 10000:
t += 1500
return round(t, 2)
Antes de mejorarla, agregamos o ejecutamos pruebas que protejan casos importantes.
El primer cambio puede ser solo de nombres:
def calcular_total_venta(productos, tipo_cliente, pais):
total = 0
for producto in productos:
total += producto["precio"] * producto["cantidad"]
if tipo_cliente == "vip":
total *= 0.85
if pais == "AR":
total *= 1.21
if total < 10000:
total += 1500
return round(total, 2)
Después ejecuta:
python -m pytest
Luego extraemos valores mágicos:
DESCUENTO_VIP = 0.15
IVA_ARGENTINA = 0.21
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500
def calcular_total_venta(productos, tipo_cliente, pais):
total = 0
for producto in productos:
total += producto["precio"] * producto["cantidad"]
if tipo_cliente == "vip":
total *= 1 - DESCUENTO_VIP
if pais == "AR":
total *= 1 + IVA_ARGENTINA
if total < LIMITE_ENVIO_GRATIS:
total += COSTO_ENVIO
return round(total, 2)
Otra vez, ejecuta las pruebas. Si fallan, el cambio no fue equivalente.
Extraemos una función pequeña:
def calcular_subtotal(productos):
subtotal = 0
for producto in productos:
subtotal += producto["precio"] * producto["cantidad"]
return subtotal
def calcular_total_venta(productos, tipo_cliente, pais):
total = calcular_subtotal(productos)
if tipo_cliente == "vip":
total *= 1 - DESCUENTO_VIP
if pais == "AR":
total *= 1 + IVA_ARGENTINA
if total < LIMITE_ENVIO_GRATIS:
total += COSTO_ENVIO
return round(total, 2)
Podemos agregar una prueba específica para calcular_subtotal.
def aplicar_descuento(total, tipo_cliente):
if tipo_cliente == "vip":
return total * (1 - DESCUENTO_VIP)
return total
def aplicar_impuesto(total, pais):
if pais == "AR":
return total * (1 + IVA_ARGENTINA)
return total
def aplicar_envio(total):
if total < LIMITE_ENVIO_GRATIS:
return total + COSTO_ENVIO
return total
La función principal queda como una secuencia:
def calcular_total_venta(productos, tipo_cliente, pais):
total = calcular_subtotal(productos)
total = aplicar_descuento(total, tipo_cliente)
total = aplicar_impuesto(total, pais)
total = aplicar_envio(total)
return round(total, 2)
Después de una serie de mejoras, ejecuta:
python -m ruff check src tests
python -m mypy src
python -m pytest
Si usas formateadores:
python -m isort src tests
python -m black src tests
Una mejora segura debería tener un diff fácil de explicar. Si el diff mezcla formato, renombres, cambios de reglas y nuevas funciones, será más difícil de revisar.
Buenas preguntas:
Conviene detenerse si:
Si descubres que una regla está mal, sepárala de la mejora estructural. Primero mejora sin cambiar comportamiento; luego haz otro cambio explícito para corregir la regla.
En ventas_demo, elige una función con smells y aplica este ciclo:
python -m pytest
# hacer un cambio pequeño
python -m pytest
python -m ruff check src tests
Registra en una nota qué smell corregiste y qué prueba protege el comportamiento.
Mejora esta función en tres pasos pequeños:
def f(items):
s = 0
for x in items:
if x["ok"]:
s += x["p"] * x["q"]
if s > 5000:
s -= 300
return s
Pasos sugeridos:
LIMITE_DESCUENTO = 5000
DESCUENTO = 300
def calcular_subtotal_aprobado(items):
subtotal = 0
for item in items:
if item["aprobado"]:
subtotal += item["precio"] * item["cantidad"]
return subtotal
def aplicar_descuento(total):
if total > LIMITE_DESCUENTO:
return total - DESCUENTO
return total
def calcular_total_aprobado(items):
subtotal = calcular_subtotal_aprobado(items)
return aplicar_descuento(subtotal)
En ventas_demo, realiza una mejora pequeña y segura:
python -m pytest
python -m ruff check src tests
Antes de continuar, verifica que puedes hacer lo siguiente:
En este tema vimos que mejorar código con seguridad requiere ritmo y disciplina: pruebas primero, cambios pequeños, verificación frecuente y revisión del diff.
En el próximo tema estudiaremos métricas de mantenibilidad y límites razonables para un proyecto Python.